summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorColin Cross <ccross@android.com>2017-03-22 18:42:49 +0000
committerandroid-build-merger <android-build-merger@google.com>2017-03-22 18:42:49 +0000
commit9dd38738a3b51b2fcaef7b39afa79d61864c45b8 (patch)
tree4287a03003b29b55dd87bd3e9d99701fcf99f34f
parent19ccfc6886359d1df55cfc0f12095298a85bfeca (diff)
parentb1838b10defb41bb0e99a99a253014b109a3af91 (diff)
downloaddesugar-9dd38738a3b51b2fcaef7b39afa79d61864c45b8.tar.gz
Merge remote-tracking branch 'aosp/upstream-master' into master am: 0b3c15f840 am: cc9e6f50aeandroid-vts-8.0_r2android-vts-8.0_r1oreo-dev
am: b1838b10de Change-Id: I65df3ab235d9746b787758e77937f17bd7691ccf
-rw-r--r--java/com/google/devtools/build/android/desugar/ClassReaderFactory.java33
-rw-r--r--java/com/google/devtools/build/android/desugar/Desugar.java189
-rw-r--r--java/com/google/devtools/build/android/desugar/DirectoryInputFileProvider.java81
-rw-r--r--java/com/google/devtools/build/android/desugar/DirectoryOutputFileProvider.java59
-rw-r--r--java/com/google/devtools/build/android/desugar/HeaderClassLoader.java17
-rw-r--r--java/com/google/devtools/build/android/desugar/IndexedInputs.java75
-rw-r--r--java/com/google/devtools/build/android/desugar/IndexedJars.java84
-rw-r--r--java/com/google/devtools/build/android/desugar/InputFileProvider.java36
-rw-r--r--java/com/google/devtools/build/android/desugar/LambdaClassMaker.java2
-rw-r--r--java/com/google/devtools/build/android/desugar/OutputFileProvider.java29
-rw-r--r--java/com/google/devtools/build/android/desugar/ZipInputFileProvider.java64
-rw-r--r--java/com/google/devtools/build/android/desugar/ZipOutputFileProvider.java76
-rw-r--r--java/com/google/devtools/common/options/Converters.java196
-rw-r--r--java/com/google/devtools/common/options/ExpansionFunction.java31
-rw-r--r--java/com/google/devtools/common/options/IsolatedOptionsData.java382
-rw-r--r--java/com/google/devtools/common/options/Option.java159
-rw-r--r--java/com/google/devtools/common/options/OptionsData.java307
-rw-r--r--java/com/google/devtools/common/options/OptionsParser.java47
-rw-r--r--java/com/google/devtools/common/options/OptionsParserImpl.java160
-rw-r--r--java/com/google/devtools/common/options/OptionsUsage.java72
20 files changed, 1378 insertions, 721 deletions
diff --git a/java/com/google/devtools/build/android/desugar/ClassReaderFactory.java b/java/com/google/devtools/build/android/desugar/ClassReaderFactory.java
index 56f99a3..2b44d76 100644
--- a/java/com/google/devtools/build/android/desugar/ClassReaderFactory.java
+++ b/java/com/google/devtools/build/android/desugar/ClassReaderFactory.java
@@ -15,19 +15,16 @@ package com.google.devtools.build.android.desugar;
import java.io.IOException;
import java.io.InputStream;
-import java.util.jar.JarFile;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipFile;
import javax.annotation.Nullable;
import org.objectweb.asm.ClassReader;
class ClassReaderFactory {
- private final IndexedJars indexedJars;
+ private final IndexedInputs indexedInputs;
private final CoreLibraryRewriter rewriter;
- public ClassReaderFactory(IndexedJars indexedJars, CoreLibraryRewriter rewriter) {
+ public ClassReaderFactory(IndexedInputs indexedInputs, CoreLibraryRewriter rewriter) {
this.rewriter = rewriter;
- this.indexedJars = indexedJars;
+ this.indexedInputs = indexedInputs;
}
/**
@@ -38,23 +35,19 @@ class ClassReaderFactory {
@Nullable
public ClassReader readIfKnown(String internalClassName) {
String filename = rewriter.unprefix(internalClassName) + ".class";
- JarFile jarFile = indexedJars.getJarFile(filename);
+ InputFileProvider inputFileProvider = indexedInputs.getInputFileProvider(filename);
- if (jarFile != null) {
- return getClassReader(internalClassName, jarFile, jarFile.getEntry(filename));
+ if (inputFileProvider != null) {
+ try (InputStream bytecode = inputFileProvider.getInputStream(filename)) {
+ // ClassReader doesn't take ownership and instead eagerly reads the stream's contents
+ return rewriter.reader(bytecode);
+ } catch (IOException e) {
+ // We should've already read through all files in the Jar once at this point, so we don't
+ // expect failures reading some files a second time.
+ throw new IllegalStateException("Couldn't load " + internalClassName, e);
+ }
}
return null;
}
-
- private ClassReader getClassReader(String internalClassName, ZipFile jar, ZipEntry entry) {
- try (InputStream bytecode = jar.getInputStream(entry)) {
- // ClassReader doesn't take ownership and instead eagerly reads the stream's contents
- return rewriter.reader(bytecode);
- } catch (IOException e) {
- // We should've already read through all files in the Jar once at this point, so we don't
- // expect failures reading some files a second time.
- throw new IllegalStateException("Couldn't load " + internalClassName, e);
- }
- }
}
diff --git a/java/com/google/devtools/build/android/desugar/Desugar.java b/java/com/google/devtools/build/android/desugar/Desugar.java
index 6f9e0f0..22780ea 100644
--- a/java/com/google/devtools/build/android/desugar/Desugar.java
+++ b/java/com/google/devtools/build/android/desugar/Desugar.java
@@ -18,14 +18,14 @@ import static java.nio.charset.StandardCharsets.ISO_8859_1;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
-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;
import com.google.devtools.common.options.Option;
import com.google.devtools.common.options.OptionsBase;
import com.google.devtools.common.options.OptionsParser;
-import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.FileVisitResult;
@@ -34,14 +34,9 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
-import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
-import java.util.zip.CRC32;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipFile;
-import java.util.zip.ZipOutputStream;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
@@ -62,8 +57,8 @@ class Desugar {
converter = ExistingPathConverter.class,
abbrev = 'i',
help =
- "Input Jar with classes to desugar (required, the n-th input is paired with the n-th "
- + "output)."
+ "Input Jar or directory with classes to desugar (required, the n-th input is paired with"
+ + "the n-th output)."
)
public List<Path> inputJars;
@@ -96,6 +91,15 @@ class Desugar {
public boolean allowEmptyBootclasspath;
@Option(
+ name = "only_desugar_javac9_for_lint",
+ defaultValue = "false",
+ help =
+ "A temporary flag specifically for android lint, subject to removal anytime (DO NOT USE)",
+ category = "undocumented"
+ )
+ public boolean onlyDesugarJavac9ForLint;
+
+ @Option(
name = "output",
allowMultiple = true,
defaultValue = "",
@@ -103,8 +107,8 @@ class Desugar {
converter = PathConverter.class,
abbrev = 'o',
help =
- "Output Jar to write desugared classes into (required, the n-th output is paired with "
- + "the n-th input)."
+ "Output Jar or directory to write desugared classes into (required, the n-th output is "
+ + "paired with the n-th input, output must be a Jar if input is a Jar)."
)
public List<Path> outputJars;
@@ -171,7 +175,12 @@ class Desugar {
"Desugar requires the same number of inputs and outputs to pair them");
checkState(!options.bootclasspath.isEmpty() || options.allowEmptyBootclasspath,
"At least one --bootclasspath_entry is required");
-
+ for (Path path : options.classpath) {
+ checkState(!Files.isDirectory(path), "Classpath entry must be a jar file: %s", path);
+ }
+ for (Path path : options.bootclasspath) {
+ checkState(!Files.isDirectory(path), "Bootclasspath entry must be a jar file: %s", path);
+ }
if (options.verbose) {
System.out.printf("Lambda classes will be written under %s%n", dumpDirectory);
}
@@ -186,72 +195,86 @@ class Desugar {
// Process each input separately
for (InputOutputPair inputOutputPair : toInputOutputPairs(options)) {
- Path inputJar = inputOutputPair.getInput();
- IndexedJars appIndexedJar = new IndexedJars(ImmutableList.of(inputJar));
- IndexedJars appAndClasspathIndexedJars = new IndexedJars(options.classpath, appIndexedJar);
- ClassLoader loader =
- createClassLoader(rewriter, options.bootclasspath, appAndClasspathIndexedJars);
-
- try (ZipFile in = new ZipFile(inputJar.toFile());
- ZipOutputStream out =
- new ZipOutputStream(
- new BufferedOutputStream(Files.newOutputStream(inputOutputPair.getOutput())))) {
+ Path inputPath = inputOutputPair.getInput();
+ Path outputPath = inputOutputPair.getOutput();
+ checkState(
+ Files.isDirectory(inputPath) || !Files.isDirectory(outputPath),
+ "Input jar file requires an output jar file");
+
+ try (Closer closer = Closer.create();
+ OutputFileProvider outputFileProvider = toOutputFileProvider(outputPath)) {
+ InputFileProvider appInputFiles = toInputFileProvider(closer, inputPath);
+ List<InputFileProvider> classpathInputFiles =
+ toInputFileProvider(closer, options.classpath);
+ IndexedInputs appIndexedInputs = new IndexedInputs(ImmutableList.of(appInputFiles));
+ IndexedInputs appAndClasspathIndexedInputs =
+ new IndexedInputs(classpathInputFiles, appIndexedInputs);
+ ClassLoader loader =
+ createClassLoader(
+ rewriter,
+ toInputFileProvider(closer, options.bootclasspath),
+ appAndClasspathIndexedInputs);
+
ClassReaderFactory readerFactory =
new ClassReaderFactory(
(options.copyBridgesFromClasspath && !allowDefaultMethods)
- ? appAndClasspathIndexedJars
- : appIndexedJar,
+ ? appAndClasspathIndexedInputs
+ : appIndexedInputs,
rewriter);
ImmutableSet.Builder<String> interfaceLambdaMethodCollector = ImmutableSet.builder();
- // Process input Jar, desugaring as we go
- for (Enumeration<? extends ZipEntry> entries = in.entries(); entries.hasMoreElements(); ) {
- ZipEntry entry = entries.nextElement();
- try (InputStream content = in.getInputStream(entry)) {
- // We can write classes uncompressed since they need to be converted to .dex format for
- // Android anyways. Resources are written as they were in the input jar to avoid any
- // danger of accidentally uncompressed resources ending up in an .apk.
- if (entry.getName().endsWith(".class")) {
+ // Process inputs, desugaring as we go
+ for (String filename : appInputFiles) {
+ try (InputStream content = appInputFiles.getInputStream(filename)) {
+ // We can write classes uncompressed since they need to be converted to .dex format
+ // for Android anyways. Resources are written as they were in the input jar to avoid
+ // any danger of accidentally uncompressed resources ending up in an .apk.
+ if (filename.endsWith(".class")) {
ClassReader reader = rewriter.reader(content);
CoreLibraryRewriter.UnprefixingClassWriter writer =
rewriter.writer(ClassWriter.COMPUTE_MAXS /*for bridge methods*/);
ClassVisitor visitor = writer;
- if (!allowDefaultMethods) {
- visitor = new Java7Compatibility(visitor, readerFactory);
- }
- visitor =
- new LambdaDesugaring(
- visitor, loader, lambdas, interfaceLambdaMethodCollector,
- allowDefaultMethods);
+ if (!options.onlyDesugarJavac9ForLint) {
+ if (!allowDefaultMethods) {
+ visitor = new Java7Compatibility(visitor, readerFactory);
+ }
+
+ visitor =
+ new LambdaDesugaring(
+ visitor,
+ loader,
+ lambdas,
+ interfaceLambdaMethodCollector,
+ allowDefaultMethods);
+ }
if (!allowCallsToObjectsNonNull) {
visitor = new ObjectsRequireNonNullMethodInliner(visitor);
}
reader.accept(visitor, 0);
- writeStoredEntry(out, entry.getName(), writer.toByteArray());
+ outputFileProvider.write(filename, writer.toByteArray());
} else {
- // TODO(bazel-team): Avoid de- and re-compressing resource files
- ZipEntry destEntry = new ZipEntry(entry);
- destEntry.setCompressedSize(-1);
- out.putNextEntry(destEntry);
- ByteStreams.copy(content, out);
- out.closeEntry();
+ outputFileProvider.copyFrom(filename, appInputFiles);
}
}
}
ImmutableSet<String> interfaceLambdaMethods = interfaceLambdaMethodCollector.build();
- if (allowDefaultMethods) {
- checkState(
- interfaceLambdaMethods.isEmpty(),
- "Desugaring with default methods enabled moved interface lambdas");
- }
+ checkState(
+ !allowDefaultMethods || interfaceLambdaMethods.isEmpty(),
+ "Desugaring with default methods enabled moved interface lambdas");
// Write out the lambda classes we generated along the way
- for (Map.Entry<Path, LambdaInfo> lambdaClass : lambdas.drain().entrySet()) {
+ ImmutableMap<Path, LambdaInfo> lambdaClasses = lambdas.drain();
+ checkState(
+ !options.onlyDesugarJavac9ForLint || lambdaClasses.isEmpty(),
+ "There should be no lambda classes generated: %s",
+ lambdaClasses.keySet());
+
+ for (Map.Entry<Path, LambdaInfo> lambdaClass : lambdaClasses.entrySet()) {
try (InputStream bytecode =
Files.newInputStream(dumpDirectory.resolve(lambdaClass.getKey()))) {
ClassReader reader = rewriter.reader(bytecode);
@@ -282,7 +305,7 @@ class Desugar {
reader.accept(visitor, 0);
String filename =
rewriter.unprefix(lambdaClass.getValue().desiredInternalName()) + ".class";
- writeStoredEntry(out, filename, writer.toByteArray());
+ outputFileProvider.write(filename, writer.toByteArray());
}
}
@@ -292,7 +315,7 @@ class Desugar {
}
}
- private static List<InputOutputPair> toInputOutputPairs(Options options) {
+ private static List<InputOutputPair> toInputOutputPairs(Options options) {
final ImmutableList.Builder<InputOutputPair> ioPairListbuilder = ImmutableList.builder();
for (Iterator<Path> inputIt = options.inputJars.iterator(),
outputIt = options.outputJars.iterator();
@@ -302,38 +325,22 @@ class Desugar {
return ioPairListbuilder.build();
}
- private static void writeStoredEntry(ZipOutputStream out, String filename, byte[] content)
+ private static ClassLoader createClassLoader(
+ CoreLibraryRewriter rewriter,
+ List<InputFileProvider> bootclasspath,
+ IndexedInputs appAndClasspathIndexedInputs)
throws IOException {
- // Need to pre-compute checksum for STORED (uncompressed) entries)
- CRC32 checksum = new CRC32();
- checksum.update(content);
-
- ZipEntry result = new ZipEntry(filename);
- result.setTime(0L); // Use stable timestamp Jan 1 1980
- result.setCrc(checksum.getValue());
- result.setSize(content.length);
- result.setCompressedSize(content.length);
- // Write uncompressed, since this is just an intermediary artifact that we will convert to .dex
- result.setMethod(ZipEntry.STORED);
-
- out.putNextEntry(result);
- out.write(content);
- out.closeEntry();
- }
-
- private static ClassLoader createClassLoader(CoreLibraryRewriter rewriter,
- List<Path> bootclasspath, IndexedJars appAndClasspathIndexedJars) throws IOException {
// 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 parent = new ThrowingClassLoader();
if (!bootclasspath.isEmpty()) {
- parent = new HeaderClassLoader(new IndexedJars(bootclasspath), rewriter, parent);
+ parent = new HeaderClassLoader(new IndexedInputs(bootclasspath), rewriter, parent);
}
// Prepend classpath with input jar itself so LambdaDesugaring can load classes with lambdas.
// Note that inputJar and classpath need to be in the same classloader because we typically get
// the header Jar for inputJar on the classpath and having the header Jar in a parent loader
// means the header version is preferred over the real thing.
- return new HeaderClassLoader(appAndClasspathIndexedJars, rewriter, parent);
+ return new HeaderClassLoader(appAndClasspathIndexedInputs, rewriter, parent);
}
private static class ThrowingClassLoader extends ClassLoader {
@@ -386,6 +393,36 @@ class Desugar {
}
}
+ /** Transform a Path to an {@link OutputFileProvider} */
+ private static OutputFileProvider toOutputFileProvider(Path path)
+ throws IOException {
+ if (Files.isDirectory(path)) {
+ return new DirectoryOutputFileProvider(path);
+ } else {
+ return new ZipOutputFileProvider(path);
+ }
+ }
+
+ /** Transform a Path to an InputFileProvider and register it to close it at the end of desugar */
+ private static InputFileProvider toInputFileProvider(Closer closer, Path path)
+ throws IOException {
+ if (Files.isDirectory(path)) {
+ return closer.register(new DirectoryInputFileProvider(path));
+ } else {
+ return closer.register(new ZipInputFileProvider(path));
+ }
+ }
+
+ private static ImmutableList<InputFileProvider> toInputFileProvider(
+ Closer closer, List<Path> paths) throws IOException {
+ ImmutableList.Builder<InputFileProvider> builder = new ImmutableList.Builder<>();
+ for (Path path : paths) {
+ checkState(!Files.isDirectory(path), "Directory is not supported: %s", path);
+ builder.add(closer.register(new ZipInputFileProvider(path)));
+ }
+ return builder.build();
+ }
+
/**
* Pair input and output.
*/
diff --git a/java/com/google/devtools/build/android/desugar/DirectoryInputFileProvider.java b/java/com/google/devtools/build/android/desugar/DirectoryInputFileProvider.java
new file mode 100644
index 0000000..1c5abc9
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/DirectoryInputFileProvider.java
@@ -0,0 +1,81 @@
+// 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 java.io.File;
+import java.io.FileInputStream;
+import java.io.IOError;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.stream.Stream;
+import java.util.zip.ZipEntry;
+
+/** Input provider is a directory. */
+class DirectoryInputFileProvider implements InputFileProvider {
+
+ private final Path root;
+
+ public DirectoryInputFileProvider(Path root) {
+ this.root = root;
+ }
+
+ @Override
+ public String toString() {
+ return root.getFileName().toString();
+ }
+
+ @Override
+ public InputStream getInputStream(String filename) throws IOException {
+ return new FileInputStream(root.resolve(filename).toFile());
+ }
+
+ @Override
+ public ZipEntry getZipEntry(String filename) {
+ ZipEntry destEntry = new ZipEntry(filename);
+ destEntry.setTime(0L); // Use stable timestamp Jan 1 1980
+ return destEntry;
+ }
+
+ @Override
+ public void close() throws IOException {
+ // Nothing to close
+ }
+
+ @Override
+ public Iterator<String> iterator() {
+ final List<String> entries = new ArrayList<>();
+ try (Stream<Path> paths = Files.walk(root)) {
+ paths.forEach(
+ new Consumer<Path>() {
+ @Override
+ public void accept(Path t) {
+ if (Files.isRegularFile(t)) {
+ // Internally, we use '/' as a common package separator in filename to abstract
+ // that filename can comes from a zip or a directory.
+ entries.add(root.relativize(t).toString().replace(File.separatorChar, '/'));
+ }
+ }
+ });
+ } catch (IOException e) {
+ throw new IOError(e);
+ }
+ return entries.iterator();
+ }
+}
diff --git a/java/com/google/devtools/build/android/desugar/DirectoryOutputFileProvider.java b/java/com/google/devtools/build/android/desugar/DirectoryOutputFileProvider.java
new file mode 100644
index 0000000..782a81e
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/DirectoryOutputFileProvider.java
@@ -0,0 +1,59 @@
+// 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 com.google.common.io.ByteStreams;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+/** Output provider is a directory. */
+public class DirectoryOutputFileProvider implements OutputFileProvider {
+
+ private final Path root;
+
+ public DirectoryOutputFileProvider(Path root) {
+ this.root = root;
+ }
+
+ @Override
+ public void copyFrom(String filename, InputFileProvider inputFileProvider) throws IOException {
+ Path path = root.resolve(filename);
+ createParentFolder(path);
+ try (InputStream is = inputFileProvider.getInputStream(filename);
+ OutputStream os = Files.newOutputStream(path)) {
+ ByteStreams.copy(is, os);
+ }
+ }
+
+ @Override
+ public void write(String filename, byte[] content) throws IOException {
+ Path path = root.resolve(filename);
+ createParentFolder(path);
+ Files.write(path, content);
+ }
+
+ @Override
+ public void close() {
+ // Nothing to close
+ }
+
+ private void createParentFolder(Path path) throws IOException {
+ if (!Files.exists(path.getParent())) {
+ Files.createDirectories(path.getParent());
+ }
+ }
+}
diff --git a/java/com/google/devtools/build/android/desugar/HeaderClassLoader.java b/java/com/google/devtools/build/android/desugar/HeaderClassLoader.java
index 44c3932..154b793 100644
--- a/java/com/google/devtools/build/android/desugar/HeaderClassLoader.java
+++ b/java/com/google/devtools/build/android/desugar/HeaderClassLoader.java
@@ -16,8 +16,6 @@ package com.google.devtools.build.android.desugar;
import java.io.IOError;
import java.io.IOException;
import java.io.InputStream;
-import java.util.jar.JarFile;
-import java.util.zip.ZipEntry;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
@@ -35,26 +33,25 @@ import org.objectweb.asm.Opcodes;
*/
class HeaderClassLoader extends ClassLoader {
- private final IndexedJars indexedJars;
+ private final IndexedInputs indexedInputs;
private final CoreLibraryRewriter rewriter;
public HeaderClassLoader(
- IndexedJars indexedJars, CoreLibraryRewriter rewriter, ClassLoader parent) {
+ IndexedInputs indexedInputs, CoreLibraryRewriter rewriter, ClassLoader parent) {
super(parent);
this.rewriter = rewriter;
- this.indexedJars = indexedJars;
+ this.indexedInputs = indexedInputs;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String filename = rewriter.unprefix(name.replace('.', '/') + ".class");
- JarFile jarfile = indexedJars.getJarFile(filename);
- if (jarfile == null) {
- throw new ClassNotFoundException();
+ InputFileProvider inputFileProvider = indexedInputs.getInputFileProvider(filename);
+ if (inputFileProvider == null) {
+ throw new ClassNotFoundException("Class " + name + " not found");
}
- ZipEntry entry = jarfile.getEntry(filename);
byte[] bytecode;
- try (InputStream content = jarfile.getInputStream(entry)) {
+ try (InputStream content = inputFileProvider.getInputStream(filename)) {
ClassReader reader = rewriter.reader(content);
// Have ASM compute maxs so we don't need to figure out how many formal parameters there are
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
diff --git a/java/com/google/devtools/build/android/desugar/IndexedInputs.java b/java/com/google/devtools/build/android/desugar/IndexedInputs.java
new file mode 100644
index 0000000..58459cc
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/IndexedInputs.java
@@ -0,0 +1,75 @@
+// 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 com.google.common.base.Preconditions;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.Nullable;
+
+/**
+ * Opens the given list of input files and compute an index of all classes in them, to avoid
+ * scanning all inputs over and over for each class to load. An indexed inputs can have a parent
+ * that is firstly used when a file name is searched.
+ */
+class IndexedInputs {
+
+ private final Map<String, InputFileProvider> inputFiles = new HashMap<>();
+
+ /** Parent indexed inputs to use before to search a file name into this indexed inputs. */
+ @Nullable
+ private final IndexedInputs parentIndexedInputs;
+
+ /** Index a list of input files without a parent indexed inputs. */
+ public IndexedInputs(List<InputFileProvider> inputProviders) throws IOException {
+ this(inputProviders, null);
+ }
+
+ /**
+ * Index a list of input files and set a parent indexed inputs that is firstly used during the
+ * search of a file name.
+ */
+ public IndexedInputs(
+ List<InputFileProvider> inputProviders, @Nullable IndexedInputs parentIndexedInputs)
+ throws IOException {
+ this.parentIndexedInputs = parentIndexedInputs;
+ for (InputFileProvider inputProvider : inputProviders) {
+ indexInput(inputProvider);
+ }
+ }
+
+ @Nullable
+ public InputFileProvider getInputFileProvider(String filename) {
+ Preconditions.checkArgument(filename.endsWith(".class"));
+
+ if (parentIndexedInputs != null) {
+ InputFileProvider inputFileProvider = parentIndexedInputs.getInputFileProvider(filename);
+ if (inputFileProvider != null) {
+ return inputFileProvider;
+ }
+ }
+
+ return inputFiles.get(filename);
+ }
+
+ private void indexInput(final InputFileProvider inputFileProvider) throws IOException {
+ for (String relativePath : inputFileProvider) {
+ if (relativePath.endsWith(".class") && !inputFiles.containsKey(relativePath)) {
+ inputFiles.put(relativePath, inputFileProvider);
+ }
+ }
+ }
+}
diff --git a/java/com/google/devtools/build/android/desugar/IndexedJars.java b/java/com/google/devtools/build/android/desugar/IndexedJars.java
deleted file mode 100644
index bdb5b47..0000000
--- a/java/com/google/devtools/build/android/desugar/IndexedJars.java
+++ /dev/null
@@ -1,84 +0,0 @@
-// 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 com.google.common.base.Preconditions;
-import java.io.IOException;
-import java.nio.file.Path;
-import java.util.Enumeration;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.jar.JarEntry;
-import java.util.jar.JarFile;
-import javax.annotation.Nullable;
-
-/**
- * Opens the given list of Jar files and compute an index of all classes in them, to avoid
- * scanning all Jars over and over for each class to load. An indexed jars can have a parent
- * that is firstly used when a file name is searched.
- */
-class IndexedJars {
-
- private final Map<String, JarFile> jarfiles = new HashMap<>();
-
- /**
- * Parent indexed jars to use before to search a file name into this indexed jars.
- */
- @Nullable
- private final IndexedJars parentIndexedJar;
-
- /**
- * Index a list of Jar files without a parent indexed jars.
- */
- public IndexedJars(List<Path> jarFiles) throws IOException {
- this(jarFiles, null);
- }
-
- /**
- * Index a list of Jar files and set a parent indexed jars that is firstly used during the search
- * of a file name.
- */
- public IndexedJars(List<Path> jarFiles, @Nullable IndexedJars parentIndexedJar)
- throws IOException {
- this.parentIndexedJar = parentIndexedJar;
- for (Path jarfile : jarFiles) {
- indexJar(jarfile);
- }
- }
-
- @Nullable
- public JarFile getJarFile(String filename) {
- Preconditions.checkArgument(filename.endsWith(".class"));
-
- if (parentIndexedJar != null) {
- JarFile jarFile = parentIndexedJar.getJarFile(filename);
- if (jarFile != null) {
- return jarFile;
- }
- }
-
- return jarfiles.get(filename);
- }
-
- private void indexJar(Path jarfile) throws IOException {
- JarFile jar = new JarFile(jarfile.toFile());
- for (Enumeration<JarEntry> cur = jar.entries(); cur.hasMoreElements(); ) {
- JarEntry entry = cur.nextElement();
- if (entry.getName().endsWith(".class") && !jarfiles.containsKey(entry.getName())) {
- jarfiles.put(entry.getName(), jar);
- }
- }
- }
-}
diff --git a/java/com/google/devtools/build/android/desugar/InputFileProvider.java b/java/com/google/devtools/build/android/desugar/InputFileProvider.java
new file mode 100644
index 0000000..c2b6353
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/InputFileProvider.java
@@ -0,0 +1,36 @@
+// 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 java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.zip.ZipEntry;
+
+/** Input file provider allows to iterate on relative path filename of a directory or a jar file. */
+interface InputFileProvider extends Closeable, Iterable<String> {
+
+ /**
+ * Return a ZipEntry for {@code filename}. If the provider is a {@link ZipInputFileProvider}, the
+ * method returns the existing ZipEntry in order to keep its metadata, otherwise a new one is
+ * created.
+ */
+ ZipEntry getZipEntry(String filename);
+
+ /**
+ * This method returns an input stream allowing to read the file {@code filename}, it is the
+ * responsibility of the caller to close this stream.
+ */
+ InputStream getInputStream(String filename) throws IOException;
+}
diff --git a/java/com/google/devtools/build/android/desugar/LambdaClassMaker.java b/java/com/google/devtools/build/android/desugar/LambdaClassMaker.java
index 2a9f9be..155e323 100644
--- a/java/com/google/devtools/build/android/desugar/LambdaClassMaker.java
+++ b/java/com/google/devtools/build/android/desugar/LambdaClassMaker.java
@@ -57,7 +57,7 @@ class LambdaClassMaker {
* Returns absolute paths to .class files generated since the last call to this method together
* with a string descriptor of the factory method.
*/
- public Map<Path, LambdaInfo> drain() {
+ public ImmutableMap<Path, LambdaInfo> drain() {
ImmutableMap<Path, LambdaInfo> result = ImmutableMap.copyOf(generatedClasses);
generatedClasses.clear();
return result;
diff --git a/java/com/google/devtools/build/android/desugar/OutputFileProvider.java b/java/com/google/devtools/build/android/desugar/OutputFileProvider.java
new file mode 100644
index 0000000..bf3b710
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/OutputFileProvider.java
@@ -0,0 +1,29 @@
+// 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 java.io.IOException;
+
+/** Output file provider allows to write files in directory or jar files. */
+interface OutputFileProvider extends AutoCloseable {
+
+ /**
+ * Copy {@code filename} from {@code inputFileProvider} to this output. If input file provider is
+ * a {@link ZipInputFileProvider} then the metadata of the zip entry are kept.
+ */
+ void copyFrom(String filename, InputFileProvider inputFileProvider) throws IOException;
+
+ /** Write {@code content} in {@code filename} to this output */
+ void write(String filename, byte[] content) throws IOException;
+}
diff --git a/java/com/google/devtools/build/android/desugar/ZipInputFileProvider.java b/java/com/google/devtools/build/android/desugar/ZipInputFileProvider.java
new file mode 100644
index 0000000..307c8b8
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/ZipInputFileProvider.java
@@ -0,0 +1,64 @@
+// 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 com.google.common.base.Functions;
+import com.google.common.collect.Iterators;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Path;
+import java.util.Iterator;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+/** Input provider is a zip file. */
+class ZipInputFileProvider implements InputFileProvider {
+
+ private final Path root;
+
+ private final ZipFile zipFile;
+
+ public ZipInputFileProvider(Path root) throws IOException {
+ this.root = root;
+ this.zipFile = new ZipFile(root.toFile());
+ }
+
+ @Override
+ public void close() throws IOException {
+ zipFile.close();
+ }
+
+ @Override
+ public String toString() {
+ return root.getFileName().toString();
+ }
+
+ @Override
+ public ZipEntry getZipEntry(String filename) {
+ ZipEntry zipEntry = zipFile.getEntry(filename);
+ zipEntry.setCompressedSize(-1);
+ return zipEntry;
+ }
+
+ @Override
+ public InputStream getInputStream(String filename) throws IOException {
+ return zipFile.getInputStream(zipFile.getEntry(filename));
+ }
+
+ @Override
+ public Iterator<String> iterator() {
+ return Iterators.transform(
+ Iterators.forEnumeration(zipFile.entries()), Functions.toStringFunction());
+ }
+}
diff --git a/java/com/google/devtools/build/android/desugar/ZipOutputFileProvider.java b/java/com/google/devtools/build/android/desugar/ZipOutputFileProvider.java
new file mode 100644
index 0000000..3f4f344
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/ZipOutputFileProvider.java
@@ -0,0 +1,76 @@
+// 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 com.google.common.base.Preconditions;
+import com.google.common.io.ByteStreams;
+import java.io.BufferedOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.zip.CRC32;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+/** Output provider is a zip file. */
+public class ZipOutputFileProvider implements OutputFileProvider {
+
+ private final ZipOutputStream out;
+
+ public ZipOutputFileProvider(Path root) throws IOException {
+ out = new ZipOutputStream(new BufferedOutputStream(Files.newOutputStream(root)));
+ }
+
+ @Override
+ public void copyFrom(String filename, InputFileProvider inputFileProvider) throws IOException {
+ // TODO(bazel-team): Avoid de- and re-compressing resource files
+ out.putNextEntry(inputFileProvider.getZipEntry(filename));
+ try (InputStream is = inputFileProvider.getInputStream(filename)) {
+ ByteStreams.copy(is, out);
+ }
+ out.closeEntry();
+ }
+
+ @Override
+ public void write(String filename, byte[] content) throws IOException {
+ Preconditions.checkArgument(filename.endsWith(".class"));
+ writeStoredEntry(out, filename, content);
+ }
+
+ @Override
+ public void close() throws IOException {
+ out.close();
+ }
+
+ private static void writeStoredEntry(ZipOutputStream out, String filename, byte[] content)
+ throws IOException {
+ // Need to pre-compute checksum for STORED (uncompressed) entries)
+ CRC32 checksum = new CRC32();
+ checksum.update(content);
+
+ ZipEntry result = new ZipEntry(filename);
+ result.setTime(0L); // Use stable timestamp Jan 1 1980
+ result.setCrc(checksum.getValue());
+ result.setSize(content.length);
+ result.setCompressedSize(content.length);
+ // Write uncompressed, since this is just an intermediary artifact that
+ // we will convert to .dex
+ result.setMethod(ZipEntry.STORED);
+
+ out.putNextEntry(result);
+ out.write(content);
+ out.closeEntry();
+ }
+}
diff --git a/java/com/google/devtools/common/options/Converters.java b/java/com/google/devtools/common/options/Converters.java
index c8b4d47..0d19029 100644
--- a/java/com/google/devtools/common/options/Converters.java
+++ b/java/com/google/devtools/common/options/Converters.java
@@ -16,7 +16,6 @@ package com.google.devtools.common.options;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
-
import java.util.Iterator;
import java.util.List;
import java.util.Map;
@@ -30,6 +29,171 @@ import java.util.regex.PatternSyntaxException;
*/
public final class Converters {
+ /** Standard converter for booleans. Accepts common shorthands/synonyms. */
+ public static class BooleanConverter implements Converter<Boolean> {
+ @Override
+ public Boolean convert(String input) throws OptionsParsingException {
+ if (input == null) {
+ return false;
+ }
+ input = input.toLowerCase();
+ if (input.equals("true")
+ || input.equals("1")
+ || input.equals("yes")
+ || input.equals("t")
+ || input.equals("y")) {
+ return true;
+ }
+ if (input.equals("false")
+ || input.equals("0")
+ || input.equals("no")
+ || input.equals("f")
+ || input.equals("n")) {
+ return false;
+ }
+ throw new OptionsParsingException("'" + input + "' is not a boolean");
+ }
+
+ @Override
+ public String getTypeDescription() {
+ return "a boolean";
+ }
+ }
+
+ /** Standard converter for Strings. */
+ public static class StringConverter implements Converter<String> {
+ @Override
+ public String convert(String input) {
+ return input;
+ }
+
+ @Override
+ public String getTypeDescription() {
+ return "a string";
+ }
+ }
+
+ /** Standard converter for integers. */
+ public static class IntegerConverter implements Converter<Integer> {
+ @Override
+ public Integer convert(String input) throws OptionsParsingException {
+ try {
+ return Integer.decode(input);
+ } catch (NumberFormatException e) {
+ throw new OptionsParsingException("'" + input + "' is not an int");
+ }
+ }
+
+ @Override
+ public String getTypeDescription() {
+ return "an integer";
+ }
+ }
+
+ /** Standard converter for longs. */
+ public static class LongConverter implements Converter<Long> {
+ @Override
+ public Long convert(String input) throws OptionsParsingException {
+ try {
+ return Long.decode(input);
+ } catch (NumberFormatException e) {
+ throw new OptionsParsingException("'" + input + "' is not a long");
+ }
+ }
+
+ @Override
+ public String getTypeDescription() {
+ return "a long integer";
+ }
+ }
+
+ /** Standard converter for doubles. */
+ public static class DoubleConverter implements Converter<Double> {
+ @Override
+ public Double convert(String input) throws OptionsParsingException {
+ try {
+ return Double.parseDouble(input);
+ } catch (NumberFormatException e) {
+ throw new OptionsParsingException("'" + input + "' is not a double");
+ }
+ }
+
+ @Override
+ public String getTypeDescription() {
+ return "a double";
+ }
+ }
+
+ /** Standard converter for TriState values. */
+ public static class TriStateConverter implements Converter<TriState> {
+ @Override
+ public TriState convert(String input) throws OptionsParsingException {
+ if (input == null) {
+ return TriState.AUTO;
+ }
+ input = input.toLowerCase();
+ if (input.equals("auto")) {
+ return TriState.AUTO;
+ }
+ if (input.equals("true")
+ || input.equals("1")
+ || input.equals("yes")
+ || input.equals("t")
+ || input.equals("y")) {
+ return TriState.YES;
+ }
+ if (input.equals("false")
+ || input.equals("0")
+ || input.equals("no")
+ || input.equals("f")
+ || input.equals("n")) {
+ return TriState.NO;
+ }
+ throw new OptionsParsingException("'" + input + "' is not a boolean");
+ }
+
+ @Override
+ public String getTypeDescription() {
+ return "a tri-state (auto, yes, no)";
+ }
+ }
+
+ /**
+ * Standard "converter" for Void. Should not actually be invoked. For instance, expansion flags
+ * are usually Void-typed and do not invoke the converter.
+ */
+ public static class VoidConverter implements Converter<Void> {
+ @Override
+ public Void convert(String input) throws OptionsParsingException {
+ if (input == null) {
+ return null; // expected input, return is unused so null is fine.
+ }
+ throw new OptionsParsingException("'" + input + "' unexpected");
+ }
+
+ @Override
+ public String getTypeDescription() {
+ return "";
+ }
+ }
+
+ /**
+ * The converters that are available to the options parser by default. These are used if the
+ * {@code @Option} annotation does not specify its own {@code converter}, and its type is one of
+ * the following.
+ */
+ static final Map<Class<?>, Converter<?>> DEFAULT_CONVERTERS = Maps.newHashMap();
+
+ static {
+ DEFAULT_CONVERTERS.put(String.class, new Converters.StringConverter());
+ DEFAULT_CONVERTERS.put(int.class, new Converters.IntegerConverter());
+ DEFAULT_CONVERTERS.put(long.class, new Converters.LongConverter());
+ DEFAULT_CONVERTERS.put(double.class, new Converters.DoubleConverter());
+ DEFAULT_CONVERTERS.put(boolean.class, new Converters.BooleanConverter());
+ DEFAULT_CONVERTERS.put(TriState.class, new Converters.TriStateConverter());
+ DEFAULT_CONVERTERS.put(Void.class, new Converters.VoidConverter());
+ }
+
/**
* Join a list of words as in English. Examples:
* "nothing"
@@ -92,7 +256,7 @@ public final class Converters {
public static class LogLevelConverter implements Converter<Level> {
- public static Level[] LEVELS = new Level[] {
+ public static final Level[] LEVELS = new Level[] {
Level.OFF, Level.SEVERE, Level.WARNING, Level.INFO, Level.FINE,
Level.FINER, Level.FINEST
};
@@ -295,32 +459,4 @@ public final class Converters {
}
}
- /**
- * A converter for boolean values. This is already one of the defaults, so clients
- * should not typically need to add this.
- */
- public static class BooleanConverter implements Converter<Boolean> {
- @Override
- public Boolean convert(String input) throws OptionsParsingException {
- if (input == null) {
- return false;
- }
- input = input.toLowerCase();
- if (input.equals("true") || input.equals("1") || input.equals("yes") ||
- input.equals("t") || input.equals("y")) {
- return true;
- }
- if (input.equals("false") || input.equals("0") || input.equals("no") ||
- input.equals("f") || input.equals("n")) {
- return false;
- }
- throw new OptionsParsingException("'" + input + "' is not a boolean");
- }
-
- @Override
- public String getTypeDescription() {
- return "a boolean";
- }
- }
-
}
diff --git a/java/com/google/devtools/common/options/ExpansionFunction.java b/java/com/google/devtools/common/options/ExpansionFunction.java
new file mode 100644
index 0000000..ffab6e7
--- /dev/null
+++ b/java/com/google/devtools/common/options/ExpansionFunction.java
@@ -0,0 +1,31 @@
+// 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;
+
+/**
+ * A function from an option parser's static setup (what flags it knows about) to an expansion
+ * String[] to use for one of its options.
+ */
+public interface ExpansionFunction {
+
+ /**
+ * Compute the expansion for an option. May be called at any time during or after the {@link
+ * OptionsParser}'s construction, or not at all.
+ *
+ * @param optionsData the parser's indexed information about its own options, before expansion
+ * information is computed
+ * @return An expansion to use for all occurrences of this option in this parser
+ */
+ public String[] getExpansion(IsolatedOptionsData optionsData);
+}
diff --git a/java/com/google/devtools/common/options/IsolatedOptionsData.java b/java/com/google/devtools/common/options/IsolatedOptionsData.java
new file mode 100644
index 0000000..27f42f4
--- /dev/null
+++ b/java/com/google/devtools/common/options/IsolatedOptionsData.java
@@ -0,0 +1,382 @@
+// Copyright 2014 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 com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * An immutable selection of options data corresponding to a set of options classes. The data is
+ * collected using reflection, which can be expensive. Therefore this class can be used internally
+ * to cache the results.
+ *
+ * <p>The data is isolated in the sense that it has not yet been processed to add inter-option-
+ * dependent information -- namely, the results of evaluating expansion functions. The {@link
+ * OptionsData} subclass stores this added information. The reason for the split is so that we can
+ * avoid exposing to expansion functions the effects of evaluating other expansion functions, to
+ * ensure that the order in which they run is not significant.
+ */
+// TODO(brandjon): This class is technically not necessarily immutable due to optionsDefault
+// accepting Object values, and the List in allOptionsField should be ImmutableList. Either fix
+// this or remove @Immutable.
+@Immutable
+class IsolatedOptionsData extends OpaqueOptionsData {
+
+ /**
+ * These are the options-declaring classes which are annotated with {@link Option} annotations.
+ */
+ private final ImmutableMap<Class<? extends OptionsBase>, Constructor<?>> optionsClasses;
+
+ /** Maps option name to Option-annotated Field. */
+ private final ImmutableMap<String, Field> nameToField;
+
+ /** Maps option abbreviation to Option-annotated Field. */
+ private final ImmutableMap<Character, Field> abbrevToField;
+
+ /** For each options class, contains a list of all Option-annotated fields in that class. */
+ private final ImmutableMap<Class<? extends OptionsBase>, List<Field>> allOptionsFields;
+
+ /** Mapping from each Option-annotated field to the default value for that field. */
+ // Immutable like the others, but uses Collections.unmodifiableMap because of null values.
+ private final Map<Field, Object> optionDefaults;
+
+ /**
+ * Mapping from each Option-annotated field to the proper converter.
+ *
+ * @see #findConverter
+ */
+ private final ImmutableMap<Field, Converter<?>> converters;
+
+ /**
+ * Mapping from each Option-annotated field to a boolean for whether that field allows multiple
+ * values.
+ */
+ private final ImmutableMap<Field, Boolean> allowMultiple;
+
+ private IsolatedOptionsData(
+ Map<Class<? extends OptionsBase>, Constructor<?>> optionsClasses,
+ Map<String, Field> nameToField,
+ Map<Character, Field> abbrevToField,
+ Map<Class<? extends OptionsBase>, List<Field>> allOptionsFields,
+ Map<Field, Object> optionDefaults,
+ Map<Field, Converter<?>> converters,
+ Map<Field, Boolean> allowMultiple) {
+ this.optionsClasses = ImmutableMap.copyOf(optionsClasses);
+ this.nameToField = ImmutableMap.copyOf(nameToField);
+ this.abbrevToField = ImmutableMap.copyOf(abbrevToField);
+ this.allOptionsFields = ImmutableMap.copyOf(allOptionsFields);
+ // Can't use an ImmutableMap here because of null values.
+ this.optionDefaults = Collections.unmodifiableMap(optionDefaults);
+ this.converters = ImmutableMap.copyOf(converters);
+ this.allowMultiple = ImmutableMap.copyOf(allowMultiple);
+ }
+
+ protected IsolatedOptionsData(IsolatedOptionsData other) {
+ this(
+ other.optionsClasses,
+ other.nameToField,
+ other.abbrevToField,
+ other.allOptionsFields,
+ other.optionDefaults,
+ other.converters,
+ other.allowMultiple);
+ }
+
+ public Collection<Class<? extends OptionsBase>> getOptionsClasses() {
+ return optionsClasses.keySet();
+ }
+
+ @SuppressWarnings("unchecked") // The construction ensures that the case is always valid.
+ public <T extends OptionsBase> Constructor<T> getConstructor(Class<T> clazz) {
+ return (Constructor<T>) optionsClasses.get(clazz);
+ }
+
+ public Field getFieldFromName(String name) {
+ return nameToField.get(name);
+ }
+
+ public Iterable<Map.Entry<String, Field>> getAllNamedFields() {
+ return nameToField.entrySet();
+ }
+
+ public Field getFieldForAbbrev(char abbrev) {
+ return abbrevToField.get(abbrev);
+ }
+
+ public List<Field> getFieldsForClass(Class<? extends OptionsBase> optionsClass) {
+ return allOptionsFields.get(optionsClass);
+ }
+
+ public Object getDefaultValue(Field field) {
+ return optionDefaults.get(field);
+ }
+
+ public Converter<?> getConverter(Field field) {
+ return converters.get(field);
+ }
+
+ public boolean getAllowMultiple(Field field) {
+ return allowMultiple.get(field);
+ }
+
+ /**
+ * 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
+ * {@code T}.
+ */
+ private static Type getFieldSingularType(Field field, Option annotation) {
+ Type fieldType = field.getGenericType();
+ 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<...>");
+ }
+ ParameterizedType pfieldType = (ParameterizedType) fieldType;
+ if (pfieldType.getRawType() != List.class) {
+ throw new AssertionError("Type of multiple occurrence option must be a List<...>");
+ }
+ fieldType = pfieldType.getActualTypeArguments()[0];
+ }
+ return fieldType;
+ }
+
+ /**
+ * Returns whether a field should be considered as boolean.
+ *
+ * <p>Can be used for usage help and controlling whether the "no" prefix is allowed.
+ */
+ static boolean isBooleanField(Field field) {
+ return field.getType().equals(boolean.class)
+ || field.getType().equals(TriState.class)
+ || findConverter(field) instanceof BoolOrEnumConverter;
+ }
+
+ /** Returns whether a field has Void type. */
+ static boolean isVoidField(Field field) {
+ return field.getType().equals(Void.class);
+ }
+
+ /**
+ * Returns whether the arg is an expansion option defined by an expansion function (and not a
+ * constant expansion value).
+ */
+ static boolean usesExpansionFunction(Option annotation) {
+ return annotation.expansionFunction() != ExpansionFunction.class;
+ }
+
+ /**
+ * Given an {@code @Option}-annotated field, retrieves the {@link Converter} that will be used,
+ * taking into account the default converters if an explicit one is not specified.
+ */
+ static Converter<?> findConverter(Field optionField) {
+ Option annotation = optionField.getAnnotation(Option.class);
+ if (annotation.converter() == Converter.class) {
+ // No converter provided, use the default one.
+ Type type = getFieldSingularType(optionField, annotation);
+ Converter<?> converter = Converters.DEFAULT_CONVERTERS.get(type);
+ if (converter == null) {
+ throw new AssertionError(
+ "No converter found for "
+ + type
+ + "; possible fix: add "
+ + "converter=... to @Option annotation for "
+ + optionField.getName());
+ }
+ return converter;
+ }
+ try {
+ // Instantiate the given Converter class.
+ Class<?> converter = annotation.converter();
+ Constructor<?> constructor = converter.getConstructor();
+ return (Converter<?>) constructor.newInstance();
+ } catch (Exception e) {
+ // This indicates an error in the Converter, and should be discovered the first time it is
+ // used.
+ throw new AssertionError(e);
+ }
+ }
+
+ private static List<Field> getAllAnnotatedFields(Class<? extends OptionsBase> optionsClass) {
+ List<Field> allFields = Lists.newArrayList();
+ for (Field field : optionsClass.getFields()) {
+ if (field.isAnnotationPresent(Option.class)) {
+ allFields.add(field);
+ }
+ }
+ if (allFields.isEmpty()) {
+ throw new IllegalStateException(optionsClass + " has no public @Option-annotated fields");
+ }
+ return ImmutableList.copyOf(allFields);
+ }
+
+ private static Object retrieveDefaultFromAnnotation(Field optionField) {
+ Converter<?> converter = findConverter(optionField);
+ String defaultValueAsString = OptionsParserImpl.getDefaultOptionString(optionField);
+ // Special case for "null"
+ if (OptionsParserImpl.isSpecialNullDefault(defaultValueAsString, optionField)) {
+ return null;
+ }
+ boolean allowsMultiple = optionField.getAnnotation(Option.class).allowMultiple();
+ // If the option allows multiple values then we intentionally return the empty list as
+ // the default value of this option since it is not always the case that an option
+ // that allows multiple values will have a converter that returns a list value.
+ if (allowsMultiple) {
+ return Collections.emptyList();
+ }
+ // Otherwise try to convert the default value using the converter
+ Object convertedValue;
+ try {
+ convertedValue = converter.convert(defaultValueAsString);
+ } catch (OptionsParsingException e) {
+ throw new IllegalStateException("OptionsParsingException while "
+ + "retrieving default for " + optionField.getName() + ": "
+ + e.getMessage());
+ }
+ return convertedValue;
+ }
+
+ /**
+ * Constructs an {@link IsolatedOptionsData} object for a parser that knows about the given
+ * {@link OptionsBase} classes. No inter-option analysis is done. Performs basic sanity checking
+ * on each option in isolation.
+ */
+ static IsolatedOptionsData from(Collection<Class<? extends OptionsBase>> classes) {
+ Map<Class<? extends OptionsBase>, Constructor<?>> constructorBuilder = Maps.newHashMap();
+ Map<Class<? extends OptionsBase>, List<Field>> allOptionsFieldsBuilder = Maps.newHashMap();
+ Map<String, Field> nameToFieldBuilder = Maps.newHashMap();
+ Map<Character, Field> abbrevToFieldBuilder = Maps.newHashMap();
+ Map<Field, Object> optionDefaultsBuilder = Maps.newHashMap();
+ Map<Field, Converter<?>> convertersBuilder = Maps.newHashMap();
+ Map<Field, Boolean> allowMultipleBuilder = Maps.newHashMap();
+
+ // Read all Option annotations:
+ for (Class<? extends OptionsBase> parsedOptionsClass : classes) {
+ try {
+ Constructor<? extends OptionsBase> constructor =
+ parsedOptionsClass.getConstructor();
+ constructorBuilder.put(parsedOptionsClass, constructor);
+ } catch (NoSuchMethodException e) {
+ throw new IllegalArgumentException(parsedOptionsClass
+ + " lacks an accessible default constructor");
+ }
+ List<Field> fields = getAllAnnotatedFields(parsedOptionsClass);
+ allOptionsFieldsBuilder.put(parsedOptionsClass, fields);
+
+ for (Field field : fields) {
+ Option annotation = field.getAnnotation(Option.class);
+
+ if (annotation.name() == null) {
+ throw new AssertionError("Option cannot have a null name");
+ }
+
+ Type fieldType = getFieldSingularType(field, annotation);
+
+ // Get the converter return type.
+ @SuppressWarnings("rawtypes")
+ Class<? extends Converter> converter = annotation.converter();
+ if (converter == Converter.class) {
+ Converter<?> actualConverter = Converters.DEFAULT_CONVERTERS.get(fieldType);
+ if (actualConverter == null) {
+ throw new AssertionError("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
+ + " must be a concrete type");
+ }
+ Type converterResultType;
+ try {
+ 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");
+ }
+
+ if (annotation.allowMultiple()) {
+ if (GenericTypeHelper.getRawType(converterResultType) == List.class) {
+ Type elementType =
+ ((ParameterizedType) converterResultType).getActualTypeArguments()[0];
+ if (!GenericTypeHelper.isAssignableFrom(fieldType, elementType)) {
+ throw new AssertionError("If the converter return type of a multiple occurance " +
+ "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 + ")");
+ }
+ }
+ } else {
+ if (!GenericTypeHelper.isAssignableFrom(fieldType, converterResultType)) {
+ throw new AssertionError("Type of field (" + fieldType +
+ ") must be assignable from the converter " +
+ "return type (" + converterResultType + ")");
+ }
+ }
+
+ if (nameToFieldBuilder.put(annotation.name(), field) != null) {
+ throw new DuplicateOptionDeclarationException(
+ "Duplicate option name: --" + annotation.name());
+ }
+ if (!annotation.oldName().isEmpty()) {
+ if (nameToFieldBuilder.put(annotation.oldName(), field) != null) {
+ throw new DuplicateOptionDeclarationException(
+ "Old option name duplicates option name: --" + annotation.oldName());
+ }
+ }
+ if (annotation.abbrev() != '\0') {
+ if (abbrevToFieldBuilder.put(annotation.abbrev(), field) != null) {
+ throw new DuplicateOptionDeclarationException(
+ "Duplicate option abbrev: -" + annotation.abbrev());
+ }
+ }
+
+ optionDefaultsBuilder.put(field, retrieveDefaultFromAnnotation(field));
+
+ convertersBuilder.put(field, findConverter(field));
+
+ allowMultipleBuilder.put(field, annotation.allowMultiple());
+ }
+ }
+
+ return new IsolatedOptionsData(
+ constructorBuilder,
+ nameToFieldBuilder,
+ abbrevToFieldBuilder,
+ allOptionsFieldsBuilder,
+ optionDefaultsBuilder,
+ convertersBuilder,
+ allowMultipleBuilder);
+ }
+}
diff --git a/java/com/google/devtools/common/options/Option.java b/java/com/google/devtools/common/options/Option.java
index 3b2ca30..249ee70 100644
--- a/java/com/google/devtools/common/options/Option.java
+++ b/java/com/google/devtools/common/options/Option.java
@@ -47,116 +47,147 @@ public @interface Option {
String valueHelp() default "";
/**
- * The default value for the option. This method should only be invoked
- * directly by the parser implementation. Any access to default values
- * should go via the parser to allow for application specific defaults.
+ * The default value for the option. This method should only be invoked directly by the parser
+ * implementation. Any access to default values should go via the parser to allow for application
+ * specific defaults.
*
- * <p>There are two reasons this is a string. Firstly, it ensures that
- * explicitly specifying this option at its default value (as printed in the
- * usage message) has the same behavior as not specifying the option at all;
- * this would be very hard to achieve if the default value was an instance of
- * type T, since we'd need to ensure that {@link #toString()} and {@link
- * #converter} were dual to each other. The second reason is more mundane
- * but also more restrictive: annotation values must be compile-time
- * constants.
+ * <p>There are two reasons this is a string. Firstly, it ensures that explicitly specifying this
+ * option at its default value (as printed in the usage message) has the same behavior as not
+ * specifying the option at all; this would be very hard to achieve if the default value was an
+ * instance of type T, since we'd need to ensure that {@link #toString()} and {@link #converter}
+ * were dual to each other. The second reason is more mundane but also more restrictive:
+ * annotation values must be compile-time constants.
*
- * <p>If an option's defaultValue() is the string "null", the option's
- * converter will not be invoked to interpret it; a null reference will be
- * used instead. (It would be nice if defaultValue could simply return null,
- * but bizarrely, the Java Language Specification does not consider null to
- * be a compile-time constant.) This special interpretation of the string
- * "null" is only applicable when computing the default value; if specified
- * on the command-line, this string will have its usual literal meaning.
+ * <p>If an option's defaultValue() is the string "null", the option's converter will not be
+ * invoked to interpret it; a null reference will be used instead. (It would be nice if
+ * defaultValue could simply return null, but bizarrely, the Java Language Specification does not
+ * consider null to be a compile-time constant.) This special interpretation of the string "null"
+ * is only applicable when computing the default value; if specified on the command-line, this
+ * string will have its usual literal meaning.
*
- * <p>The default value for flags that set allowMultiple is always the empty
- * list and its default value is ignored.
+ * <p>The default value for flags that set allowMultiple is always the empty list and its default
+ * value is ignored.
*/
String defaultValue();
/**
* A string describing the category of options that this belongs to. {@link
- * OptionsParser#describeOptions} prints options of the same category grouped
- * together.
+ * OptionsParser#describeOptions} prints options of the same category grouped together.
+ *
+ * <p>There are three special category values:
+ *
+ * <ul>
+ * <li>{@code "undocumented"}: options which are useful for (some subset of) users, but not
+ * meant to be publicly advertised. For example, experimental options which are only meant
+ * to be used by specific testers or team members, but which should otherwise be treated
+ * normally. These options will not be listed in the usage info displayed for the {@code
+ * --help} option. They are otherwise normal - {@link
+ * OptionsParser.UnparsedOptionValueDescription#isHidden()} returns {@code false} for them,
+ * and they can be parsed normally from the command line or RC files.
+ * <li>{@code "hidden"}: options which users should not pass or know about, but which are used
+ * by the program (e.g., communication between a command-line client and a backend server).
+ * Like {@code "undocumented"} options, these options will not be listed in the usage info
+ * displayed for the {@code --help} option. However, in addition to this, calling {@link
+ * OptionsParser.UnparsedOptionValueDescription#isHidden()} on these options will return
+ * {@code true} - for example, this can be checked to strip out such secret options when
+ * logging or otherwise reporting the command line to the user. This category does not
+ * affect the option in any other way; it can still be parsed normally from the command line
+ * or an RC file.
+ * <li>{@code "internal"}: options which are purely for internal use within the JVM, and should
+ * never be shown to the user, nor ever need to be parsed by the options parser. Like {@code
+ * "hidden"} options, these options will not be listed in the usage info displayed for the
+ * --help option, and are considered hidden by {@link
+ * OptionsParser.UnparsedOptionValueDescription#isHidden()}. Unlike those, this type of
+ * option cannot be parsed by any call to {@link OptionsParser#parse} - it will be treated
+ * as if it was not defined.
+ * </ul>
*/
String category() default "misc";
/**
- * The converter that we'll use to convert this option into an object or
- * a simple type. The default is to use the builtin converters.
- * Custom converters must implement the {@link Converter} interface.
+ * The converter that we'll use to convert the string representation of this option's value into
+ * an object or a simple type. The default is to use the builtin converters ({@link
+ * Converters#DEFAULT_CONVERTERS}). Custom converters must implement the {@link Converter}
+ * interface.
*/
@SuppressWarnings({"unchecked", "rawtypes"})
// Can't figure out how to coerce Converter.class into Class<? extends Converter<?>>
Class<? extends Converter> converter() default Converter.class;
/**
- * A flag indicating whether the option type should be allowed to occur
- * multiple times in a single option list.
+ * A flag indicating whether the option type should be allowed to occur multiple times in a single
+ * option list.
*
- * <p>If the command can occur multiple times, then the attribute value
- * <em>must</em> be a list type {@code List<T>}, and the result type of the
- * converter for this option must either match the parameter {@code T} or
- * {@code List<T>}. In the latter case the individual lists are concatenated
- * to form the full options value.
+ * <p>If the command can occur multiple times, then the attribute value <em>must</em> be a list
+ * type {@code List<T>}, and the result type of the converter for this option must either match
+ * the parameter {@code T} or {@code List<T>}. In the latter case the individual lists are
+ * concatenated to form the full options value.
*
- * <p>The {@link #defaultValue()} field of the annotation is ignored for repeatable
- * flags and the default value will be the empty list.
+ * <p>The {@link #defaultValue()} field of the annotation is ignored for repeatable flags and the
+ * default value will be the empty list.
*/
boolean allowMultiple() default false;
/**
- * If the option is actually an abbreviation for other options, this field will
- * contain the strings to expand this option into. The original option is dropped
- * and the replacement used in its stead. It is recommended that such an option be
- * of type {@link Void}.
+ * If the option is actually an abbreviation for other options, this field will contain the
+ * strings to expand this option into. The original option is dropped and the replacement used in
+ * its stead. It is recommended that such an option be of type {@link Void}.
*
- * An expanded option overrides previously specified options of the same name,
- * even if it is explicitly specified. This is the original behavior and can
- * be surprising if the user is not aware of it, which has led to several
- * requests to change this behavior. This was discussed in the blaze team and
- * it was decided that it is not a strong enough case to change the behavior.
+ * <p>An expanded option overrides previously specified options of the same name, even if it is
+ * explicitly specified. This is the original behavior and can be surprising if the user is not
+ * aware of it, which has led to several requests to change this behavior. This was discussed in
+ * the blaze team and it was decided that it is not a strong enough case to change the behavior.
*/
String[] expansion() default {};
/**
- * If the option requires that additional options be implicitly appended, this field
- * will contain the additional options. Implicit dependencies are parsed at the end
- * of each {@link OptionsParser#parse} invocation, and override options specified in
- * the same call. However, they can be overridden by options specified in a later
- * call or by options with a higher priority.
+ * A mechanism for specifying an expansion that is a function of the parser's {@link
+ * IsolatedOptionsData}. This can be used to create an option that expands to different strings
+ * depending on what other options the parser knows about.
+ *
+ * <p>If provided (i.e. not {@link ExpansionFunction}{@code .class}), the {@code expansion} field
+ * must not be set. The mechanism of expansion is as if the {@code expansion} field were set to
+ * whatever the return value of this function is.
+ */
+ Class<? extends ExpansionFunction> expansionFunction() default ExpansionFunction.class;
+
+ /**
+ * If the option requires that additional options be implicitly appended, this field will contain
+ * the additional options. Implicit dependencies are parsed at the end of each {@link
+ * OptionsParser#parse} invocation, and override options specified in the same call. However, they
+ * can be overridden by options specified in a later call or by options with a higher priority.
*
* @see OptionPriority
*/
String[] implicitRequirements() default {};
/**
- * If this field is a non-empty string, the option is deprecated, and a
- * deprecation warning is added to the list of warnings when such an option
- * is used.
+ * If this field is a non-empty string, the option is deprecated, and a deprecation warning is
+ * added to the list of warnings when such an option is used.
*/
String deprecationWarning() default "";
/**
- * The old name for this option. If an option has a name "foo" and an old name "bar",
- * --foo=baz and --bar=baz will be equivalent. If the old name is used, a warning will be printed
- * indicating that the old name is deprecated and the new name should be used.
+ * The old name for this option. If an option has a name "foo" and an old name "bar", --foo=baz
+ * and --bar=baz will be equivalent. If the old name is used, a warning will be printed indicating
+ * that the old name is deprecated and the new name should be used.
*/
String oldName() default "";
/**
- * Indicates that this option is a wrapper for other options, and will be unwrapped
- * when parsed. For example, if foo is a wrapper option, then "--foo=--bar=baz"
- * will be parsed as the flag "--bar=baz" (rather than --foo taking the value
- * "--bar=baz"). A wrapper option should have the type {@link Void} (if it is something other
- * than Void, the parser will not assign a value to it). The
- * {@link Option#implicitRequirements()}, {@link Option#expansion()}, {@link Option#converter()}
- * attributes will not be processed. Wrapper options are implicitly repeatable (i.e., as though
- * {@link Option#allowMultiple()} is true regardless of its value in the annotation).
+ * Indicates that this option is a wrapper for other options, and will be unwrapped when parsed.
+ * For example, if foo is a wrapper option, then "--foo=--bar=baz" will be parsed as the flag
+ * "--bar=baz" (rather than --foo taking the value "--bar=baz"). A wrapper option should have the
+ * type {@link Void} (if it is something other than Void, the parser will not assign a value to
+ * it). The {@link Option#implicitRequirements()}, {@link Option#expansion()}, {@link
+ * Option#converter()} attributes will not be processed. Wrapper options are implicitly repeatable
+ * (i.e., as though {@link Option#allowMultiple()} is true regardless of its value in the
+ * annotation).
*
* <p>Wrapper options are provided only for transitioning flags which appear as values to other
* flags, to top-level flags. Wrapper options should not be used in Invocation Policy, as
- * expansion flags to other flags, or as implicit requirements to other flags. Use the inner
- * flags instead.
+ * expansion flags to other flags, or as implicit requirements to other flags. Use the inner flags
+ * instead.
*/
boolean wrapperOption() default false;
}
diff --git a/java/com/google/devtools/common/options/OptionsData.java b/java/com/google/devtools/common/options/OptionsData.java
index ac23d63..e71321c 100644
--- a/java/com/google/devtools/common/options/OptionsData.java
+++ b/java/com/google/devtools/common/options/OptionsData.java
@@ -1,4 +1,4 @@
-// Copyright 2014 The Bazel Authors. All rights reserved.
+// 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.
@@ -14,280 +14,89 @@
package com.google.devtools.common.options;
-import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
-import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
-import java.lang.reflect.ParameterizedType;
-import java.lang.reflect.Type;
import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
import java.util.Map;
-
import javax.annotation.concurrent.Immutable;
/**
- * An immutable selection of options data corresponding to a set of options
- * classes. The data is collected using reflection, which can be expensive.
- * Therefore this class can be used internally to cache the results.
+ * This extends IsolatedOptionsData with information that can only be determined once all the {@link
+ * OptionsBase} subclasses for a parser are known. In particular, this includes expansion
+ * information.
*/
@Immutable
-final class OptionsData extends OpaqueOptionsData {
-
- /**
- * These are the options-declaring classes which are annotated with
- * {@link Option} annotations.
- */
- private final Map<Class<? extends OptionsBase>, Constructor<?>> optionsClasses;
-
- /** Maps option name to Option-annotated Field. */
- private final Map<String, Field> nameToField;
-
- /** Maps option abbreviation to Option-annotated Field. */
- private final Map<Character, Field> abbrevToField;
+final class OptionsData extends IsolatedOptionsData {
/**
- * For each options class, contains a list of all Option-annotated fields in
- * that class.
+ * Mapping from each Option-annotated field with a {@code String[]} expansion to that expansion.
*/
- private final Map<Class<? extends OptionsBase>, List<Field>> allOptionsFields;
+ // TODO(brandjon): This is technically not necessarily immutable due to String[], and should use
+ // ImmutableList. Either fix this or remove @Immutable.
+ private final ImmutableMap<Field, String[]> evaluatedExpansions;
+
+ /** Construct {@link OptionsData} by extending an {@link IsolatedOptionsData} with new info. */
+ private OptionsData(IsolatedOptionsData base, Map<Field, String[]> evaluatedExpansions) {
+ super(base);
+ this.evaluatedExpansions = ImmutableMap.copyOf(evaluatedExpansions);
+ }
- /**
- * Mapping from each Option-annotated field to the default value for that
- * field.
- */
- private final Map<Field, Object> optionDefaults;
+ private static final String[] EMPTY_EXPANSION = new String[] {};
/**
- * Mapping from each Option-annotated field to the proper converter.
- *
- * @see OptionsParserImpl#findConverter
+ * Returns the expansion of an options field, regardless of whether it was defined using {@link
+ * Option#expansion} or {@link Option#expansionFunction}. If the field is not an expansion option,
+ * returns an empty array.
*/
- private final Map<Field, Converter<?>> converters;
+ public String[] getEvaluatedExpansion(Field field) {
+ String[] result = evaluatedExpansions.get(field);
+ return result != null ? result : EMPTY_EXPANSION;
+ }
/**
- * Mapping from each Option-annotated field to a boolean for whether that field allows multiple
- * values.
+ * Constructs an {@link OptionsData} object for a parser that knows about the given {@link
+ * OptionsBase} classes. In addition to the work done to construct the {@link
+ * IsolatedOptionsData}, this also computes expansion information.
*/
- private final Map<Field, Boolean> allowMultiple;
-
- private OptionsData(Map<Class<? extends OptionsBase>, Constructor<?>> optionsClasses,
- Map<String, Field> nameToField,
- Map<Character, Field> abbrevToField,
- Map<Class<? extends OptionsBase>, List<Field>> allOptionsFields,
- Map<Field, Object> optionDefaults,
- Map<Field, Converter<?>> converters,
- Map<Field, Boolean> allowMultiple) {
- this.optionsClasses = ImmutableMap.copyOf(optionsClasses);
- this.allOptionsFields = ImmutableMap.copyOf(allOptionsFields);
- this.nameToField = ImmutableMap.copyOf(nameToField);
- this.abbrevToField = ImmutableMap.copyOf(abbrevToField);
- // Can't use an ImmutableMap here because of null values.
- this.optionDefaults = Collections.unmodifiableMap(optionDefaults);
- this.converters = ImmutableMap.copyOf(converters);
- this.allowMultiple = ImmutableMap.copyOf(allowMultiple);
- }
-
- public Collection<Class<? extends OptionsBase>> getOptionsClasses() {
- return optionsClasses.keySet();
- }
-
- @SuppressWarnings("unchecked") // The construction ensures that the case is always valid.
- public <T extends OptionsBase> Constructor<T> getConstructor(Class<T> clazz) {
- return (Constructor<T>) optionsClasses.get(clazz);
- }
-
- public Field getFieldFromName(String name) {
- return nameToField.get(name);
- }
-
- public Iterable<Map.Entry<String, Field>> getAllNamedFields() {
- return nameToField.entrySet();
- }
-
- public Field getFieldForAbbrev(char abbrev) {
- return abbrevToField.get(abbrev);
- }
-
- public List<Field> getFieldsForClass(Class<? extends OptionsBase> optionsClass) {
- return allOptionsFields.get(optionsClass);
- }
-
- public Object getDefaultValue(Field field) {
- return optionDefaults.get(field);
- }
-
- public Converter<?> getConverter(Field field) {
- return converters.get(field);
- }
-
- public boolean getAllowMultiple(Field field) {
- return allowMultiple.get(field);
- }
-
- private static List<Field> getAllAnnotatedFields(Class<? extends OptionsBase> optionsClass) {
- List<Field> allFields = Lists.newArrayList();
- for (Field field : optionsClass.getFields()) {
- if (field.isAnnotationPresent(Option.class)) {
- allFields.add(field);
- }
- }
- if (allFields.isEmpty()) {
- throw new IllegalStateException(optionsClass + " has no public @Option-annotated fields");
- }
- return ImmutableList.copyOf(allFields);
- }
-
- private static Object retrieveDefaultFromAnnotation(Field optionField) {
- Converter<?> converter = OptionsParserImpl.findConverter(optionField);
- String defaultValueAsString = OptionsParserImpl.getDefaultOptionString(optionField);
- // Special case for "null"
- if (OptionsParserImpl.isSpecialNullDefault(defaultValueAsString, optionField)) {
- return null;
- }
- boolean allowsMultiple = optionField.getAnnotation(Option.class).allowMultiple();
- // If the option allows multiple values then we intentionally return the empty list as
- // the default value of this option since it is not always the case that an option
- // that allows multiple values will have a converter that returns a list value.
- if (allowsMultiple) {
- return Collections.emptyList();
- }
- // Otherwise try to convert the default value using the converter
- Object convertedValue;
- try {
- convertedValue = converter.convert(defaultValueAsString);
- } catch (OptionsParsingException e) {
- throw new IllegalStateException("OptionsParsingException while "
- + "retrieving default for " + optionField.getName() + ": "
- + e.getMessage());
- }
- return convertedValue;
- }
-
- static OptionsData of(Collection<Class<? extends OptionsBase>> classes) {
- Map<Class<? extends OptionsBase>, Constructor<?>> constructorBuilder = Maps.newHashMap();
- Map<Class<? extends OptionsBase>, List<Field>> allOptionsFieldsBuilder = Maps.newHashMap();
- Map<String, Field> nameToFieldBuilder = Maps.newHashMap();
- Map<Character, Field> abbrevToFieldBuilder = Maps.newHashMap();
- Map<Field, Object> optionDefaultsBuilder = Maps.newHashMap();
- Map<Field, Converter<?>> convertersBuilder = Maps.newHashMap();
- Map<Field, Boolean> allowMultipleBuilder = Maps.newHashMap();
-
- // Read all Option annotations:
- for (Class<? extends OptionsBase> parsedOptionsClass : classes) {
- try {
- Constructor<? extends OptionsBase> constructor =
- parsedOptionsClass.getConstructor(new Class[0]);
- constructorBuilder.put(parsedOptionsClass, constructor);
- } catch (NoSuchMethodException e) {
- throw new IllegalArgumentException(parsedOptionsClass
- + " lacks an accessible default constructor");
- }
- List<Field> fields = getAllAnnotatedFields(parsedOptionsClass);
- allOptionsFieldsBuilder.put(parsedOptionsClass, fields);
-
- for (Field field : fields) {
- Option annotation = field.getAnnotation(Option.class);
-
- // Check that the field type is a List, and that the converter
- // type matches the element type of the list.
- Type fieldType = field.getGenericType();
- if (annotation.allowMultiple()) {
- if (!(fieldType instanceof ParameterizedType)) {
- throw new AssertionError("Type of multiple occurrence option must be a List<...>");
- }
- ParameterizedType pfieldType = (ParameterizedType) fieldType;
- if (pfieldType.getRawType() != List.class) {
- // Throw an assertion, because this indicates an undetected type
- // error in the code.
- throw new AssertionError("Type of multiple occurrence option must be a List<...>");
- }
- fieldType = pfieldType.getActualTypeArguments()[0];
- }
-
- // Get the converter return type.
- @SuppressWarnings("rawtypes")
- Class<? extends Converter> converter = annotation.converter();
- if (converter == Converter.class) {
- Converter<?> actualConverter = OptionsParserImpl.DEFAULT_CONVERTERS.get(fieldType);
- if (actualConverter == null) {
- throw new AssertionError("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
- + ") must be a concrete type");
- }
- Type converterResultType;
- try {
- 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");
- }
-
- if (annotation.allowMultiple()) {
- if (GenericTypeHelper.getRawType(converterResultType) == List.class) {
- Type elementType =
- ((ParameterizedType) converterResultType).getActualTypeArguments()[0];
- if (!GenericTypeHelper.isAssignableFrom(fieldType, elementType)) {
- throw new AssertionError("If the converter return type of a multiple occurance " +
- "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 + ")");
- }
- }
- } else {
- if (!GenericTypeHelper.isAssignableFrom(fieldType, converterResultType)) {
- throw new AssertionError("Type of field (" + fieldType +
- ") must be assignable from the converter " +
- "return type (" + converterResultType + ")");
- }
- }
-
- if (annotation.name() == null) {
+ public static OptionsData from(Collection<Class<? extends OptionsBase>> classes) {
+ IsolatedOptionsData isolatedData = IsolatedOptionsData.from(classes);
+
+ // All that's left is to compute expansions.
+ Map<Field, String[]> evaluatedExpansionsBuilder = Maps.newHashMap();
+ for (Map.Entry<String, Field> entry : isolatedData.getAllNamedFields()) {
+ Field field = entry.getValue();
+ Option annotation = field.getAnnotation(Option.class);
+ // Determine either the hard-coded expansion, or the ExpansionFunction class.
+ String[] constExpansion = annotation.expansion();
+ Class<? extends ExpansionFunction> expansionFunctionClass = annotation.expansionFunction();
+ if (constExpansion.length > 0 && usesExpansionFunction(annotation)) {
+ throw new AssertionError(
+ "Cannot set both expansion and expansionFunction for option --" + annotation.name());
+ } else if (constExpansion.length > 0) {
+ evaluatedExpansionsBuilder.put(field, constExpansion);
+ } else if (usesExpansionFunction(annotation)) {
+ if (Modifier.isAbstract(expansionFunctionClass.getModifiers())) {
throw new AssertionError(
- "Option cannot have a null name");
+ "The expansionFunction type " + expansionFunctionClass + " must be a concrete type");
}
- if (nameToFieldBuilder.put(annotation.name(), field) != null) {
- throw new DuplicateOptionDeclarationException(
- "Duplicate option name: --" + annotation.name());
- }
- if (!annotation.oldName().isEmpty()) {
- if (nameToFieldBuilder.put(annotation.oldName(), field) != null) {
- throw new DuplicateOptionDeclarationException(
- "Old option name duplicates option name: --" + annotation.oldName());
- }
- }
- if (annotation.abbrev() != '\0') {
- if (abbrevToFieldBuilder.put(annotation.abbrev(), field) != null) {
- throw new DuplicateOptionDeclarationException(
- "Duplicate option abbrev: -" + annotation.abbrev());
- }
+ // Evaluate the ExpansionFunction.
+ ExpansionFunction instance;
+ try {
+ Constructor<?> constructor = expansionFunctionClass.getConstructor();
+ instance = (ExpansionFunction) constructor.newInstance();
+ } catch (Exception e) {
+ // This indicates an error in the ExpansionFunction, and should be discovered the first
+ // time it is used.
+ throw new AssertionError(e);
}
-
- optionDefaultsBuilder.put(field, retrieveDefaultFromAnnotation(field));
-
- convertersBuilder.put(field, OptionsParserImpl.findConverter(field));
-
- allowMultipleBuilder.put(field, annotation.allowMultiple());
+ String[] expansion = instance.getExpansion(isolatedData);
+ evaluatedExpansionsBuilder.put(field, expansion);
}
}
- return new OptionsData(constructorBuilder, nameToFieldBuilder, abbrevToFieldBuilder,
- allOptionsFieldsBuilder, optionDefaultsBuilder, convertersBuilder, allowMultipleBuilder);
+
+ return new OptionsData(isolatedData, evaluatedExpansionsBuilder);
}
}
diff --git a/java/com/google/devtools/common/options/OptionsParser.java b/java/com/google/devtools/common/options/OptionsParser.java
index 946d73b..1c4b278 100644
--- a/java/com/google/devtools/common/options/OptionsParser.java
+++ b/java/com/google/devtools/common/options/OptionsParser.java
@@ -88,7 +88,7 @@ public class OptionsParser implements OptionsProvider {
ImmutableList<Class<? extends OptionsBase>> optionsClasses) {
OptionsData result = optionsData.get(optionsClasses);
if (result == null) {
- result = OptionsData.of(optionsClasses);
+ result = OptionsData.from(optionsClasses);
optionsData.put(optionsClasses, result);
}
return result;
@@ -140,10 +140,6 @@ public class OptionsParser implements OptionsProvider {
private final List<String> residue = new ArrayList<String>();
private boolean allowResidue = true;
- OptionsParser(Collection<Class<? extends OptionsBase>> optionsClasses) {
- this(OptionsData.of(optionsClasses));
- }
-
OptionsParser(OptionsData optionsData) {
impl = new OptionsParserImpl(optionsData);
}
@@ -395,12 +391,14 @@ public class OptionsParser implements OptionsProvider {
}
public boolean isHidden() {
- return documentationLevel() == DocumentationLevel.HIDDEN;
+ return documentationLevel() == DocumentationLevel.HIDDEN
+ || documentationLevel() == DocumentationLevel.INTERNAL;
}
boolean isExpansion() {
Option option = field.getAnnotation(Option.class);
- return option.expansion().length > 0;
+ return (option.expansion().length > 0
+ || OptionsData.usesExpansionFunction(option));
}
boolean isImplicitRequirement() {
@@ -455,28 +453,27 @@ public class OptionsParser implements OptionsProvider {
*
* <p>We use 'hidden' so that options that form the protocol between the
* client and the server are not logged.
+ *
+ * <p>Options which are 'internal' are not recognized by the parser at all.
*/
enum DocumentationLevel {
- DOCUMENTED, UNDOCUMENTED, HIDDEN
+ DOCUMENTED, UNDOCUMENTED, HIDDEN, INTERNAL
}
/**
- * Returns a description of all the options this parser can digest.
- * In addition to {@link Option} annotations, this method also
- * interprets {@link OptionsUsage} annotations which give an intuitive short
- * description for the options.
+ * Returns a description of all the options this parser can digest. In addition to {@link Option}
+ * annotations, this method also interprets {@link OptionsUsage} annotations which give an
+ * intuitive short description for the options. Options of the same category (see {@link
+ * Option#category}) will be grouped together.
*
- * @param categoryDescriptions a mapping from category names to category
- * descriptions. Options of the same category (see {@link
- * Option#category}) will be grouped together, preceded by the description
- * of the category.
- * @param helpVerbosity if {@code long}, the options will be described
- * verbosely, including their types, defaults and descriptions. If {@code
- * medium}, the descriptions are omitted, and if {@code short}, the options
- * are just enumerated.
+ * @param categoryDescriptions a mapping from category names to category descriptions.
+ * Descriptions are optional; if omitted, a string based on the category name will be used.
+ * @param helpVerbosity if {@code long}, the options will be described verbosely, including their
+ * types, defaults and descriptions. If {@code medium}, the descriptions are omitted, and if
+ * {@code short}, the options are just enumerated.
*/
- public String describeOptions(Map<String, String> categoryDescriptions,
- HelpVerbosity helpVerbosity) {
+ public String describeOptions(
+ Map<String, String> categoryDescriptions, HelpVerbosity helpVerbosity) {
StringBuilder desc = new StringBuilder();
if (!impl.getOptionsClasses().isEmpty()) {
List<Field> allFields = Lists.newArrayList();
@@ -500,7 +497,7 @@ public class OptionsParser implements OptionsProvider {
}
if (documentationLevel(prevCategory) == DocumentationLevel.DOCUMENTED) {
- OptionsUsage.getUsage(optionField, desc, helpVerbosity);
+ OptionsUsage.getUsage(optionField, desc, helpVerbosity, impl.getOptionsData());
}
}
}
@@ -545,7 +542,7 @@ public class OptionsParser implements OptionsProvider {
}
if (level == DocumentationLevel.DOCUMENTED) {
- OptionsUsage.getUsageHtml(optionField, desc, escaper);
+ OptionsUsage.getUsageHtml(optionField, desc, escaper, impl.getOptionsData());
}
}
desc.append("</dl>\n");
@@ -614,6 +611,8 @@ public class OptionsParser implements OptionsProvider {
return DocumentationLevel.UNDOCUMENTED;
} else if ("hidden".equals(category)) {
return DocumentationLevel.HIDDEN;
+ } else if ("internal".equals(category)) {
+ return DocumentationLevel.INTERNAL;
} else {
return DocumentationLevel.DOCUMENTED;
}
diff --git a/java/com/google/devtools/common/options/OptionsParserImpl.java b/java/com/google/devtools/common/options/OptionsParserImpl.java
index c15f927..5c6498a 100644
--- a/java/com/google/devtools/common/options/OptionsParserImpl.java
+++ b/java/com/google/devtools/common/options/OptionsParserImpl.java
@@ -32,8 +32,6 @@ import com.google.devtools.common.options.OptionsParser.OptionValueDescription;
import com.google.devtools.common.options.OptionsParser.UnparsedOptionValueDescription;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
-import java.lang.reflect.ParameterizedType;
-import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
@@ -50,100 +48,6 @@ import java.util.Map;
*/
class OptionsParserImpl {
- /**
- * A bunch of default converters in case the user doesn't specify a
- * different one in the field annotation.
- */
- static final Map<Class<?>, Converter<?>> DEFAULT_CONVERTERS = Maps.newHashMap();
-
- static {
- DEFAULT_CONVERTERS.put(String.class, new Converter<String>() {
- @Override
- public String convert(String input) {
- return input;
- }
- @Override
- public String getTypeDescription() {
- return "a string";
- }});
- DEFAULT_CONVERTERS.put(int.class, new Converter<Integer>() {
- @Override
- public Integer convert(String input) throws OptionsParsingException {
- try {
- return Integer.decode(input);
- } catch (NumberFormatException e) {
- throw new OptionsParsingException("'" + input + "' is not an int");
- }
- }
- @Override
- public String getTypeDescription() {
- return "an integer";
- }});
- DEFAULT_CONVERTERS.put(double.class, new Converter<Double>() {
- @Override
- public Double convert(String input) throws OptionsParsingException {
- try {
- return Double.parseDouble(input);
- } catch (NumberFormatException e) {
- throw new OptionsParsingException("'" + input + "' is not a double");
- }
- }
- @Override
- public String getTypeDescription() {
- return "a double";
- }});
- DEFAULT_CONVERTERS.put(boolean.class, new Converters.BooleanConverter());
- DEFAULT_CONVERTERS.put(TriState.class, new Converter<TriState>() {
- @Override
- public TriState convert(String input) throws OptionsParsingException {
- if (input == null) {
- return TriState.AUTO;
- }
- input = input.toLowerCase();
- if (input.equals("auto")) {
- return TriState.AUTO;
- }
- if (input.equals("true") || input.equals("1") || input.equals("yes") ||
- input.equals("t") || input.equals("y")) {
- return TriState.YES;
- }
- if (input.equals("false") || input.equals("0") || input.equals("no") ||
- input.equals("f") || input.equals("n")) {
- return TriState.NO;
- }
- throw new OptionsParsingException("'" + input + "' is not a boolean");
- }
- @Override
- public String getTypeDescription() {
- return "a tri-state (auto, yes, no)";
- }});
- DEFAULT_CONVERTERS.put(Void.class, new Converter<Void>() {
- @Override
- public Void convert(String input) throws OptionsParsingException {
- if (input == null) {
- return null; // expected input, return is unused so null is fine.
- }
- throw new OptionsParsingException("'" + input + "' unexpected");
- }
- @Override
- public String getTypeDescription() {
- return "";
- }});
- DEFAULT_CONVERTERS.put(long.class, new Converter<Long>() {
- @Override
- public Long convert(String input) throws OptionsParsingException {
- try {
- return Long.decode(input);
- } catch (NumberFormatException e) {
- throw new OptionsParsingException("'" + input + "' is not a long");
- }
- }
- @Override
- public String getTypeDescription() {
- return "a long integer";
- }});
- }
-
private final OptionsData optionsData;
/**
@@ -196,6 +100,10 @@ class OptionsParserImpl {
this.optionsData = optionsData;
}
+ OptionsData getOptionsData() {
+ return optionsData;
+ }
+
/**
* Indicates whether or not the parser will allow long options with a
* single-dash, instead of the usual double-dash, too, eg. -example instead of just --example.
@@ -440,7 +348,8 @@ class OptionsParserImpl {
// Recurse to remove any implicit or expansion flags that this flag may have added when
// originally parsed.
- for (String[] args : new String[][] {option.implicitRequirements(), option.expansion()}) {
+ String[] expansion = optionsData.getEvaluatedExpansion(field);
+ for (String[] args : new String[][] {option.implicitRequirements(), expansion}) {
Iterator<String> argsIterator = Iterators.forArray(args);
while (argsIterator.hasNext()) {
String arg = argsIterator.next();
@@ -578,7 +487,8 @@ class OptionsParserImpl {
}
// Handle expansion options.
- if (option.expansion().length > 0) {
+ String[] expansion = optionsData.getEvaluatedExpansion(field);
+ if (expansion.length > 0) {
Function<Object, String> expansionSourceFunction =
Functions.constant(
"expanded from option --"
@@ -587,7 +497,7 @@ class OptionsParserImpl {
+ sourceFunction.apply(originalName));
maybeAddDeprecationWarning(field);
List<String> unparsed = parse(priority, expansionSourceFunction, null, originalName,
- ImmutableList.copyOf(option.expansion()));
+ ImmutableList.copyOf(expansion));
if (!unparsed.isEmpty()) {
// Throw an assertion, because this indicates an error in the code that specified the
// expansion for the current option.
@@ -701,7 +611,7 @@ class OptionsParserImpl {
booleanValue = false;
if (field != null) {
// TODO(bazel-team): Add tests for these cases.
- if (!OptionsParserImpl.isBooleanField(field)) {
+ if (!OptionsData.isBooleanField(field)) {
throw new OptionsParsingException(
"Illegal use of 'no' prefix on non-boolean option: " + arg, arg);
}
@@ -717,15 +627,18 @@ class OptionsParserImpl {
throw new OptionsParsingException("Invalid options syntax: " + arg, arg);
}
- if (field == null) {
+ Option option = field == null ? null : field.getAnnotation(Option.class);
+
+ if (option == null
+ || OptionsParser.documentationLevel(option.category())
+ == OptionsParser.DocumentationLevel.INTERNAL) {
+ // This also covers internal options, which are treated as if they did not exist.
throw new OptionsParsingException("Unrecognized option: " + arg, arg);
}
- Option option = field.getAnnotation(Option.class);
-
if (value == null) {
// Special-case boolean to supply value based on presence of "no" prefix.
- if (OptionsParserImpl.isBooleanField(field)) {
+ if (OptionsData.isBooleanField(field)) {
value = booleanValue ? "1" : "0";
} else if (field.getType().equals(Void.class) && !option.wrapperOption()) {
// This is expected, Void type options have no args (unless they're wrapper options).
@@ -782,46 +695,7 @@ class OptionsParserImpl {
return annotation.defaultValue();
}
- static boolean isBooleanField(Field field) {
- return field.getType().equals(boolean.class)
- || field.getType().equals(TriState.class)
- || findConverter(field) instanceof BoolOrEnumConverter;
- }
-
- static boolean isVoidField(Field field) {
- return field.getType().equals(Void.class);
- }
-
static boolean isSpecialNullDefault(String defaultValueString, Field optionField) {
return defaultValueString.equals("null") && !optionField.getType().isPrimitive();
}
-
- static Converter<?> findConverter(Field optionField) {
- Option annotation = optionField.getAnnotation(Option.class);
- if (annotation.converter() == Converter.class) {
- Type type;
- if (annotation.allowMultiple()) {
- // The OptionParserImpl already checked that the type is List<T> for some T;
- // here we extract the type T.
- type = ((ParameterizedType) optionField.getGenericType()).getActualTypeArguments()[0];
- } else {
- type = optionField.getType();
- }
- Converter<?> converter = DEFAULT_CONVERTERS.get(type);
- if (converter == null) {
- throw new AssertionError("No converter found for "
- + type + "; possible fix: add "
- + "converter=... to @Option annotation for "
- + optionField.getName());
- }
- return converter;
- }
- try {
- Class<?> converter = annotation.converter();
- Constructor<?> constructor = converter.getConstructor(new Class<?>[0]);
- return (Converter<?>) constructor.newInstance(new Object[0]);
- } catch (Exception e) {
- throw new AssertionError(e);
- }
- }
}
diff --git a/java/com/google/devtools/common/options/OptionsUsage.java b/java/com/google/devtools/common/options/OptionsUsage.java
index b8c19df..aa48cb7 100644
--- a/java/com/google/devtools/common/options/OptionsUsage.java
+++ b/java/com/google/devtools/common/options/OptionsUsage.java
@@ -13,19 +13,17 @@
// limitations under the License.
package com.google.devtools.common.options;
-import static com.google.devtools.common.options.OptionsParserImpl.findConverter;
-
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.escape.Escaper;
-
import java.lang.reflect.Field;
import java.text.BreakIterator;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
+import javax.annotation.Nullable;
/**
* A renderer for usage messages. For now this is very simple.
@@ -36,15 +34,17 @@ class OptionsUsage {
private static final Joiner COMMA_JOINER = Joiner.on(",");
/**
- * Given an options class, render the usage string into the usage,
- * which is passed in as an argument.
+ * Given an options class, render the usage string into the usage, which is passed in as an
+ * argument. This will not include information about expansions for options using expansion
+ * functions (it would be unsafe to report this as we cannot know what options from other {@link
+ * OptionsBase} subclasses they depend on until a complete parser is constructed).
*/
static void getUsage(Class<? extends OptionsBase> optionsClass, StringBuilder usage) {
List<Field> optionFields =
Lists.newArrayList(OptionsParser.getAllAnnotatedFields(optionsClass));
Collections.sort(optionFields, BY_NAME);
for (Field optionField : optionFields) {
- getUsage(optionField, usage, OptionsParser.HelpVerbosity.LONG);
+ getUsage(optionField, usage, OptionsParser.HelpVerbosity.LONG, null);
}
}
@@ -79,10 +79,35 @@ class OptionsUsage {
}
/**
- * Append the usage message for a single option-field message to 'usage'.
+ * Returns the expansion for an option, to the extent known. Precisely, if an {@link OptionsData}
+ * object is supplied, the expansion is read from that. Otherwise, the annotation is inspected: If
+ * the annotation uses {@link Option#expansion} it is returned, and if it uses {@link
+ * Option#expansionFunction} null is returned, indicating a lack of definite information. In all
+ * cases, when the option is not an expansion option, an empty array is returned.
+ */
+ private static @Nullable String[] getExpansionIfKnown(
+ Field optionField, Option annotation, @Nullable OptionsData optionsData) {
+ if (optionsData != null) {
+ return optionsData.getEvaluatedExpansion(optionField);
+ } else {
+ if (OptionsData.usesExpansionFunction(annotation)) {
+ return null;
+ } else {
+ // Empty array if it's not an expansion option.
+ return annotation.expansion();
+ }
+ }
+ }
+
+ /**
+ * Appends the usage message for a single option-field message to 'usage'. If {@code optionsData}
+ * is not supplied, options that use expansion functions won't be fully described.
*/
- static void getUsage(Field optionField, StringBuilder usage,
- OptionsParser.HelpVerbosity helpVerbosity) {
+ static void getUsage(
+ Field optionField,
+ StringBuilder usage,
+ OptionsParser.HelpVerbosity helpVerbosity,
+ @Nullable OptionsData optionsData) {
String flagName = getFlagName(optionField);
String typeDescription = getTypeDescription(optionField);
Option annotation = optionField.getAnnotation(Option.class);
@@ -117,9 +142,12 @@ class OptionsUsage {
usage.append(paragraphFill(annotation.help(), 4, 80)); // (indent, width)
usage.append('\n');
}
- if (annotation.expansion().length > 0) {
+ String[] expansion = getExpansionIfKnown(optionField, annotation, optionsData);
+ if (expansion == null) {
+ usage.append(" Expands to unknown options.\n");
+ } else if (expansion.length > 0) {
StringBuilder expandsMsg = new StringBuilder("Expands to: ");
- for (String exp : annotation.expansion()) {
+ for (String exp : expansion) {
expandsMsg.append(exp).append(" ");
}
usage.append(paragraphFill(expandsMsg.toString(), 4, 80)); // (indent, width)
@@ -128,9 +156,11 @@ class OptionsUsage {
}
/**
- * Append the usage message for a single option-field message to 'usage'.
+ * Append the usage message for a single option-field message to 'usage'. If {@code optionsData}
+ * is not supplied, options that use expansion functions won't be fully described.
*/
- static void getUsageHtml(Field optionField, StringBuilder usage, Escaper escaper) {
+ static void getUsageHtml(
+ Field optionField, StringBuilder usage, Escaper escaper, @Nullable OptionsData optionsData) {
String plainFlagName = optionField.getAnnotation(Option.class).name();
String flagName = getFlagName(optionField);
String valueDescription = optionField.getAnnotation(Option.class).valueHelp();
@@ -138,8 +168,7 @@ class OptionsUsage {
Option annotation = optionField.getAnnotation(Option.class);
usage.append("<dt><code><a name=\"flag--").append(plainFlagName).append("\"></a>--");
usage.append(flagName);
- if (OptionsParserImpl.isBooleanField(optionField)
- || OptionsParserImpl.isVoidField(optionField)) {
+ if (OptionsData.isBooleanField(optionField) || OptionsData.isVoidField(optionField)) {
// Nothing for boolean, tristate, boolean_or_enum, or void options.
} else if (!valueDescription.isEmpty()) {
usage.append("=").append(escaper.escape(valueDescription));
@@ -157,7 +186,7 @@ class OptionsUsage {
} else {
// Don't call the annotation directly (we must allow overrides to certain defaults).
String defaultValueString = OptionsParserImpl.getDefaultOptionString(optionField);
- if (OptionsParserImpl.isVoidField(optionField)) {
+ if (OptionsData.isVoidField(optionField)) {
// Void options don't have a default.
} else if (OptionsParserImpl.isSpecialNullDefault(defaultValueString, optionField)) {
usage.append(" default: see description");
@@ -171,10 +200,13 @@ class OptionsUsage {
usage.append(paragraphFill(escaper.escape(annotation.help()), 0, 80)); // (indent, width)
usage.append('\n');
}
- if (annotation.expansion().length > 0) {
+ String[] expansion = getExpansionIfKnown(optionField, annotation, optionsData);
+ if (expansion == null) {
+ usage.append(" Expands to unknown options.<br>\n");
+ } else if (expansion.length > 0) {
usage.append("<br/>\n");
StringBuilder expandsMsg = new StringBuilder("Expands to:<br/>\n");
- for (String exp : annotation.expansion()) {
+ for (String exp : expansion) {
// TODO(ulfjack): Can we link to the expanded flags here?
expandsMsg
.append("&nbsp;&nbsp;<code>")
@@ -259,12 +291,12 @@ class OptionsUsage {
};
private static String getTypeDescription(Field optionsField) {
- return findConverter(optionsField).getTypeDescription();
+ return OptionsData.findConverter(optionsField).getTypeDescription();
}
static String getFlagName(Field field) {
String name = field.getAnnotation(Option.class).name();
- return OptionsParserImpl.isBooleanField(field) ? "[no]" + name : name;
+ return OptionsData.isBooleanField(field) ? "[no]" + name : name;
}
}