summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorandroid-build-team Robot <android-build-team-robot@google.com>2017-05-17 08:01:22 +0000
committerandroid-build-team Robot <android-build-team-robot@google.com>2017-05-17 08:01:22 +0000
commiteba736429e76922f94ecb5d16737cc7827401d9a (patch)
tree293694e1293dbc4326919e772e3ffb8722985924
parent19ec33977159326b7e3ed2562071a351a32540a6 (diff)
parent86f6f67af2035e113a2bc4ba4c23709481862acc (diff)
downloaddesugar-eba736429e76922f94ecb5d16737cc7827401d9a.tar.gz
release-request-655284b0-7f0b-48b5-b01d-27f48a99f2c0-for-git_oc-mr1-release-4017105 snap-temp-L82600000064873863
Change-Id: I0bb134d4f6b032f7bed900b00521f5d07079e5fb
-rw-r--r--copy.bara.sky4
-rw-r--r--java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java156
-rw-r--r--java/com/google/devtools/build/android/desugar/Desugar.java43
-rw-r--r--java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java55
-rw-r--r--java/com/google/devtools/common/options/OptionsParser.java9
5 files changed, 201 insertions, 66 deletions
diff --git a/copy.bara.sky b/copy.bara.sky
index bc83110..0f9df66 100644
--- a/copy.bara.sky
+++ b/copy.bara.sky
@@ -1,5 +1,5 @@
# Copybara project config for importing desugar tool from bazel
-import_url = "rpc://github/bazelbuild/bazel"
+import_url = "rpc://github.googlesource.com/bazelbuild/bazel"
core.workflow(
name = "default",
@@ -19,7 +19,7 @@ core.workflow(
],
),
destination = git.gerrit_destination(
- url = "sso://android-review/platform/external/desugar",
+ url = "rpc://android-review.googlesource.com/platform/external/desugar",
fetch = "upstream-master",
push_to_refs_for = "upstream-master",
),
diff --git a/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java b/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java
index 8ad5dc2..c42062a 100644
--- a/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java
+++ b/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java
@@ -18,7 +18,10 @@ import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import com.google.common.collect.ImmutableList;
+import java.util.Comparator;
import java.util.HashSet;
+import java.util.Set;
+import java.util.TreeSet;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
@@ -33,18 +36,20 @@ public class DefaultMethodClassFixer extends ClassVisitor {
private final ClassReaderFactory classpath;
private final ClassReaderFactory bootclasspath;
+ private final ClassLoader targetLoader;
private final HashSet<String> instanceMethods = new HashSet<>();
- private final HashSet<String> seenInterfaces = new HashSet<>();
private boolean isInterface;
- private ImmutableList<String> interfaces;
+ private String internalName;
+ private ImmutableList<String> directInterfaces;
private String superName;
public DefaultMethodClassFixer(ClassVisitor dest, ClassReaderFactory classpath,
- ClassReaderFactory bootclasspath) {
+ ClassReaderFactory bootclasspath, ClassLoader targetLoader) {
super(Opcodes.ASM5, dest);
this.classpath = classpath;
this.bootclasspath = bootclasspath;
+ this.targetLoader = targetLoader;
}
@Override
@@ -55,22 +60,23 @@ public class DefaultMethodClassFixer extends ClassVisitor {
String signature,
String superName,
String[] interfaces) {
- checkState(this.interfaces == null);
+ checkState(this.directInterfaces == null);
isInterface = BitFlags.isSet(access, Opcodes.ACC_INTERFACE);
+ internalName = name;
checkArgument(superName != null || "java/lang/Object".equals(name), // ASM promises this
"Type without superclass: %s", name);
- this.interfaces = ImmutableList.copyOf(interfaces);
+ this.directInterfaces = ImmutableList.copyOf(interfaces);
this.superName = superName;
super.visit(version, access, name, signature, superName, interfaces);
}
@Override
public void visitEnd() {
- if (!isInterface && defaultMethodsDefined(interfaces)) {
+ if (!isInterface && defaultMethodsDefined(directInterfaces)) {
// Inherited methods take precedence over default methods, so visit all superclasses and
// figure out what methods they declare before stubbing in any missing default methods.
recordInheritedMethods();
- stubMissingDefaultMethods(interfaces);
+ stubMissingDefaultMethods();
}
super.visitEnd();
}
@@ -85,6 +91,51 @@ public class DefaultMethodClassFixer extends ClassVisitor {
return super.visitMethod(access, name, desc, signature, exceptions);
}
+ private void stubMissingDefaultMethods() {
+ TreeSet<Class<?>> allInterfaces = new TreeSet<>(InterfaceComparator.INSTANCE);
+ for (String direct : directInterfaces) {
+ // Loading ensures all transitively implemented interfaces can be loaded, which is necessary
+ // to produce correct default method stubs in all cases. We could do without classloading but
+ // it's convenient to rely on Class.isAssignableFrom to compute subtype relationships, and
+ // we'd still have to insist that all transitively implemented interfaces can be loaded.
+ // We don't load the visited class, however, in case it's a generated lambda class.
+ Class<?> itf = loadFromInternal(direct);
+ collectInterfaces(itf, allInterfaces);
+ }
+
+ Class<?> superclass = loadFromInternal(superName);
+ for (Class<?> interfaceToVisit : allInterfaces) {
+ // if J extends I, J is allowed to redefine I's default methods. The comparator we used
+ // above makes sure we visit J before I in that case so we can use J's definition.
+ if (superclass != null && interfaceToVisit.isAssignableFrom(superclass)) {
+ // superclass already implements this interface, so we must skip it. The superclass will
+ // be similarly rewritten or comes from the bootclasspath; either way we don't need to and
+ // shouldn't stub default methods for this interface.
+ continue;
+ }
+ stubMissingDefaultMethods(interfaceToVisit.getName().replace('.', '/'));
+ }
+ }
+
+ private Class<?> loadFromInternal(String internalName) {
+ try {
+ return targetLoader.loadClass(internalName.replace('/', '.'));
+ } catch (ClassNotFoundException e) {
+ throw new IllegalStateException(
+ "Couldn't load " + internalName + ", is the classpath complete?", e);
+ }
+ }
+
+ private void collectInterfaces(Class<?> itf, Set<Class<?>> dest) {
+ checkArgument(itf.isInterface());
+ if (!dest.add(itf)) {
+ return;
+ }
+ for (Class<?> implemented : itf.getInterfaces()) {
+ collectInterfaces(implemented, dest);
+ }
+ }
+
private void recordInheritedMethods() {
InstanceMethodRecorder recorder = new InstanceMethodRecorder();
String internalName = superName;
@@ -132,29 +183,36 @@ public class DefaultMethodClassFixer extends ClassVisitor {
return false;
}
- private void stubMissingDefaultMethods(ImmutableList<String> interfaces) {
- for (String implemented : interfaces) {
- if (!seenInterfaces.add(implemented)) {
- // Skip: a superclass already implements this interface, or we've seen it here
- continue;
- }
- ClassReader bytecode = classpath.readIfKnown(implemented);
- if (bytecode != null && !bootclasspath.isKnown(implemented)) {
- // Class in classpath and bootclasspath is a bad idea but in any event, assume the
- // bootclasspath will take precedence like in a classloader.
- // We can skip code attributes as we just need to find default methods to stub.
- bytecode.accept(new DefaultMethodStubber(), ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG);
- }
+ /**
+ * Returns {@code true} for non-bridge default methods not in {@link #instanceMethods}.
+ */
+ private boolean shouldStub(int access, String name, String desc) {
+ // Ignore private methods, which technically aren't default methods and can only be called from
+ // other methods defined in the interface. This also ignores lambda body methods, which is fine
+ // as we don't want or need to stub those. Also ignore bridge methods as javac adds them to
+ // concrete classes as needed anyway and we handle them separately for generated lambda classes.
+ return BitFlags.noneSet(access,
+ Opcodes.ACC_ABSTRACT | Opcodes.ACC_STATIC | Opcodes.ACC_BRIDGE | Opcodes.ACC_PRIVATE)
+ && !instanceMethods.contains(name + ":" + desc);
+ }
+
+ private void stubMissingDefaultMethods(String implemented) {
+ if (bootclasspath.isKnown(implemented)) {
+ // Default methods on the bootclasspath will be available at runtime, so just ignore them.
+ return;
}
+ ClassReader bytecode = checkNotNull(classpath.readIfKnown(implemented),
+ "Couldn't find interface %s implemented by %s", implemented, internalName);
+ // We can skip code attributes as we just need to find default methods to stub.
+ bytecode.accept(new DefaultMethodStubber(), ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG);
}
/**
* Visitor for interfaces that produces delegates in the class visited by the outer
* {@link DefaultMethodClassFixer} for every default method encountered.
*/
- public class DefaultMethodStubber extends ClassVisitor {
+ private class DefaultMethodStubber extends ClassVisitor {
- @SuppressWarnings("hiding") private ImmutableList<String> interfaces;
private String interfaceName;
public DefaultMethodStubber() {
@@ -170,25 +228,23 @@ public class DefaultMethodClassFixer extends ClassVisitor {
String superName,
String[] interfaces) {
checkArgument(BitFlags.isSet(access, Opcodes.ACC_INTERFACE));
- checkState(this.interfaces == null);
- this.interfaces = ImmutableList.copyOf(interfaces);
+ checkState(interfaceName == null);
interfaceName = name;
}
@Override
- public void visitEnd() {
- stubMissingDefaultMethods(this.interfaces);
- }
-
- @Override
public MethodVisitor visitMethod(
int access, String name, String desc, String signature, String[] exceptions) {
- if (BitFlags.noneSet(access, Opcodes.ACC_ABSTRACT | Opcodes.ACC_STATIC | Opcodes.ACC_BRIDGE)
- && !instanceMethods.contains(name + ":" + desc)) {
+ if (shouldStub(access, name, desc)) {
+ // Remember we stubbed this method in case it's also defined by subsequently visited
+ // interfaces. javac would force the method to be defined explicitly if there any two
+ // definitions conflict, but see stubMissingDefaultMethods() for how we deal with default
+ // methods redefined in interfaces extending another.
+ recordIfInstanceMethod(access, name, desc);
+
// Add this method to the class we're desugaring and stub in a body to call the default
// implementation in the interface's companion class. ijar omits these methods when setting
- // ACC_SYNTHETIC modifier, so don't. Don't do this for bridge methods, which we handle
- // separately.
+ // ACC_SYNTHETIC modifier, so don't.
// Signatures can be wrong, e.g., when type variables are introduced, instantiated, or
// refined in the class we're processing, so drop them.
MethodVisitor stubMethod =
@@ -219,7 +275,7 @@ public class DefaultMethodClassFixer extends ClassVisitor {
/**
* Visitor for interfaces that recursively searches interfaces for default method declarations.
*/
- public class DefaultMethodFinder extends ClassVisitor {
+ private class DefaultMethodFinder extends ClassVisitor {
@SuppressWarnings("hiding") private ImmutableList<String> interfaces;
private boolean found;
@@ -255,8 +311,7 @@ public class DefaultMethodClassFixer extends ClassVisitor {
@Override
public MethodVisitor visitMethod(
int access, String name, String desc, String signature, String[] exceptions) {
- if (BitFlags.noneSet(access, Opcodes.ACC_ABSTRACT | Opcodes.ACC_STATIC | Opcodes.ACC_BRIDGE)
- && !instanceMethods.contains(name + ":" + desc)) {
+ if (!found && shouldStub(access, name, desc)) {
// Found a default method we're not ignoring (instanceMethods at this point contains methods
// the top-level visited class implements itself).
found = true;
@@ -280,13 +335,6 @@ public class DefaultMethodClassFixer extends ClassVisitor {
String superName,
String[] interfaces) {
checkArgument(BitFlags.noneSet(access, Opcodes.ACC_INTERFACE));
- for (String inheritedInterface : interfaces) {
- // No point copying default methods that we'll also copy for a superclass. Note we may
- // be processing a class in the bootclasspath, in which case the interfaces must also
- // be in the bootclasspath and we can skip those as well. Also note this is best-effort,
- // since these interfaces may extend other interfaces that we're not recording here.
- seenInterfaces.add(inheritedInterface);
- }
super.visit(version, access, name, signature, superName, interfaces);
}
@@ -297,4 +345,26 @@ public class DefaultMethodClassFixer extends ClassVisitor {
return null;
}
}
+
+ /** Comparator for interfaces that compares by whether interfaces extend one another. */
+ enum InterfaceComparator implements Comparator<Class<?>> {
+ INSTANCE;
+
+ @Override
+ public int compare(Class<?> o1, Class<?> o2) {
+ checkArgument(o1.isInterface());
+ checkArgument(o2.isInterface());
+ if (o1 == o2) {
+ return 0;
+ }
+ if (o1.isAssignableFrom(o2)) { // o1 is supertype of o2
+ return 1; // we want o1 to come after o2
+ }
+ if (o2.isAssignableFrom(o1)) { // o2 is supertype of o1
+ return -1; // we want o2 to come after o1
+ }
+ // o1 and o2 aren't comparable so arbitrarily impose lexicographical ordering
+ return o1.getName().compareTo(o2.getName());
+ }
+ }
}
diff --git a/java/com/google/devtools/build/android/desugar/Desugar.java b/java/com/google/devtools/build/android/desugar/Desugar.java
index c4528ae..b4b4c8e 100644
--- a/java/com/google/devtools/build/android/desugar/Desugar.java
+++ b/java/com/google/devtools/build/android/desugar/Desugar.java
@@ -42,9 +42,11 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
+import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.Nullable;
import org.objectweb.asm.ClassReader;
@@ -171,6 +173,16 @@ class Desugar {
public boolean desugarTryWithResourcesIfNeeded;
@Option(
+ name = "desugar_try_with_resources_omit_runtime_classes",
+ defaultValue = "false",
+ category = "misc",
+ help =
+ "Omits the runtime classes necessary to support try-with-resources from the output. "
+ + "This property has effect only if --desugar_try_with_resources_if_needed is used."
+ )
+ public boolean desugarTryWithResourcesOmitRuntimeClasses;
+
+ @Option(
name = "copy_bridges_from_classpath",
defaultValue = "false",
category = "misc",
@@ -193,6 +205,7 @@ class Desugar {
private final CoreLibraryRewriter rewriter;
private final LambdaClassMaker lambdas;
private final GeneratedClassStore store;
+ private final Set<String> visitedExceptionTypes = new HashSet<>();
/** The counter to record the times of try-with-resources desugaring is invoked. */
private final AtomicInteger numOfTryWithResourcesInvoked = new AtomicInteger();
@@ -306,7 +319,9 @@ class Desugar {
}
private void copyThrowableExtensionClass(OutputFileProvider outputFileProvider) {
- if (!outputJava7 || !options.desugarTryWithResourcesIfNeeded) {
+ if (!outputJava7
+ || !options.desugarTryWithResourcesIfNeeded
+ || options.desugarTryWithResourcesOmitRuntimeClasses) {
// try-with-resources statements are okay in the output jar.
return;
}
@@ -441,10 +456,13 @@ class Desugar {
// null ClassReaderFactory b/c we don't expect to need it for lambda classes
visitor = new Java7Compatibility(visitor, (ClassReaderFactory) null);
if (options.desugarTryWithResourcesIfNeeded) {
- visitor = new TryWithResourcesRewriter(visitor, loader, numOfTryWithResourcesInvoked);
+ visitor =
+ new TryWithResourcesRewriter(
+ visitor, loader, visitedExceptionTypes, numOfTryWithResourcesInvoked);
}
if (options.desugarInterfaceMethodBodiesIfNeeded) {
- visitor = new DefaultMethodClassFixer(visitor, classpathReader, bootclasspathReader);
+ visitor =
+ new DefaultMethodClassFixer(visitor, classpathReader, bootclasspathReader, loader);
visitor = new InterfaceDesugaring(visitor, bootclasspathReader, store);
}
}
@@ -487,10 +505,13 @@ class Desugar {
if (outputJava7) {
visitor = new Java7Compatibility(visitor, classpathReader);
if (options.desugarTryWithResourcesIfNeeded) {
- visitor = new TryWithResourcesRewriter(visitor, loader, numOfTryWithResourcesInvoked);
+ visitor =
+ new TryWithResourcesRewriter(
+ visitor, loader, visitedExceptionTypes, numOfTryWithResourcesInvoked);
}
if (options.desugarInterfaceMethodBodiesIfNeeded) {
- visitor = new DefaultMethodClassFixer(visitor, classpathReader, bootclasspathReader);
+ visitor =
+ new DefaultMethodClassFixer(visitor, classpathReader, bootclasspathReader, loader);
visitor = new InterfaceDesugaring(visitor, bootclasspathReader, store);
}
}
@@ -522,15 +543,19 @@ class Desugar {
* LambdaClassMaker generates lambda classes for us, but it does so by essentially simulating the
* call to LambdaMetafactory that the JVM would make when encountering an invokedynamic.
* LambdaMetafactory is in the JDK and its implementation has a property to write out ("dump")
- * generated classes, which we take advantage of here. Set property before doing anything else
- * since the property is read in the static initializer; if this breaks we can investigate setting
- * the property when calling the tool.
+ * generated classes, which we take advantage of here. This property can be set externally, and in
+ * that case the specified directory is used as a temporary dir. Otherwise, it will be set here,
+ * before doing anything else since the property is read in the static initializer.
*/
private static Path createAndRegisterLambdaDumpDirectory() throws IOException {
+ String propertyValue = System.getProperty(LambdaClassMaker.LAMBDA_METAFACTORY_DUMPER_PROPERTY);
+ if (propertyValue != null) {
+ return Paths.get(propertyValue);
+ }
+
Path dumpDirectory = Files.createTempDirectory("lambdas");
System.setProperty(
LambdaClassMaker.LAMBDA_METAFACTORY_DUMPER_PROPERTY, dumpDirectory.toString());
-
deleteTreeOnExit(dumpDirectory);
return dumpDirectory;
}
diff --git a/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java b/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java
index 2429d2f..38255a1 100644
--- a/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java
+++ b/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java
@@ -13,6 +13,7 @@
// limitations under the License.
package com.google.devtools.build.android.desugar;
+import static com.google.common.base.Preconditions.checkNotNull;
import static org.objectweb.asm.Opcodes.ASM5;
import static org.objectweb.asm.Opcodes.INVOKESTATIC;
import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
@@ -22,8 +23,11 @@ import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
+import java.util.Collections;
+import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
/**
@@ -83,34 +87,69 @@ public class TryWithResourcesRewriter extends ClassVisitor {
.build();
private final ClassLoader classLoader;
-
+ private final Set<String> visitedExceptionTypes;
private final AtomicInteger numOfTryWithResourcesInvoked;
+ /**
+ * Indicate whether the current class being desugared should be ignored. If the current class is
+ * one of the runtime extension classes, then it should be ignored.
+ */
+ private boolean shouldCurrentClassBeIgnored;
public TryWithResourcesRewriter(
ClassVisitor classVisitor,
ClassLoader classLoader,
+ Set<String> visitedExceptionTypes,
AtomicInteger numOfTryWithResourcesInvoked) {
super(ASM5, classVisitor);
this.classLoader = classLoader;
+ this.visitedExceptionTypes = visitedExceptionTypes;
this.numOfTryWithResourcesInvoked = numOfTryWithResourcesInvoked;
}
@Override
+ public void visit(
+ int version,
+ int access,
+ String name,
+ String signature,
+ String superName,
+ String[] interfaces) {
+ super.visit(version, access, name, signature, superName, interfaces);
+ shouldCurrentClassBeIgnored = THROWABLE_EXT_CLASS_INTERNAL_NAMES.contains(name);
+ }
+
+ @Override
public MethodVisitor visitMethod(
int access, String name, String desc, String signature, String[] exceptions) {
+ if (exceptions != null && exceptions.length > 0) {
+ // collect exception types.
+ Collections.addAll(visitedExceptionTypes, exceptions);
+ }
MethodVisitor visitor = super.cv.visitMethod(access, name, desc, signature, exceptions);
- return visitor == null || THROWABLE_EXT_CLASS_INTERNAL_NAMES.contains(name)
+ return visitor == null || shouldCurrentClassBeIgnored
? visitor
- : new TryWithResourceVisitor(visitor, classLoader);
+ : new TryWithResourceVisitor(name + desc, visitor, classLoader);
}
private class TryWithResourceVisitor extends MethodVisitor {
private final ClassLoader classLoader;
+ /** For debugging purpose. Enrich exception information. */
+ private final String methodSignature;
- public TryWithResourceVisitor(MethodVisitor methodVisitor, ClassLoader classLoader) {
+ public TryWithResourceVisitor(
+ String methodSignature, MethodVisitor methodVisitor, ClassLoader classLoader) {
super(ASM5, methodVisitor);
this.classLoader = classLoader;
+ this.methodSignature = methodSignature;
+ }
+
+ @Override
+ public void visitTryCatchBlock(Label start, Label end, Label handler, String type) {
+ if (type != null) {
+ visitedExceptionTypes.add(type); // type in a try-catch block must extend Throwable.
+ }
+ super.visitTryCatchBlock(start, end, handler, type);
}
@Override
@@ -120,6 +159,7 @@ public class TryWithResourcesRewriter extends ClassVisitor {
return;
}
numOfTryWithResourcesInvoked.incrementAndGet();
+ visitedExceptionTypes.add(checkNotNull(owner)); // owner extends Throwable.
super.visitMethodInsn(
INVOKESTATIC, THROWABLE_EXTENSION_INTERNAL_NAME, name, METHOD_DESC_MAP.get(desc), false);
}
@@ -131,15 +171,16 @@ public class TryWithResourcesRewriter extends ClassVisitor {
if (!TARGET_METHODS.containsEntry(name, desc)) {
return false;
}
- if (owner.equals("java/lang/Throwable")) {
- return true; // early return, for performance.
+ if (visitedExceptionTypes.contains(owner)) {
+ return true; // The owner is an exception that has been visited before.
}
try {
Class<?> throwableClass = classLoader.loadClass("java.lang.Throwable");
Class<?> klass = classLoader.loadClass(owner.replace('/', '.'));
return throwableClass.isAssignableFrom(klass);
} catch (ClassNotFoundException e) {
- throw new AssertionError(e);
+ throw new AssertionError(
+ "Failed to load class when desugaring method " + methodSignature, e);
}
}
}
diff --git a/java/com/google/devtools/common/options/OptionsParser.java b/java/com/google/devtools/common/options/OptionsParser.java
index c6bd002..728c490 100644
--- a/java/com/google/devtools/common/options/OptionsParser.java
+++ b/java/com/google/devtools/common/options/OptionsParser.java
@@ -553,15 +553,14 @@ public class OptionsParser implements OptionsProvider {
for (Field optionField : allFields) {
Option option = optionField.getAnnotation(Option.class);
String category = option.category();
- if (!category.equals(prevCategory)) {
- prevCategory = category;
+ if (!category.equals(prevCategory)
+ && option.optionUsageRestrictions() == OptionUsageRestrictions.DOCUMENTED) {
String description = categoryDescriptions.get(category);
if (description == null) {
description = "Options category '" + category + "'";
}
- if (option.optionUsageRestrictions() == OptionUsageRestrictions.DOCUMENTED) {
- desc.append("\n").append(description).append(":\n");
- }
+ desc.append("\n").append(description).append(":\n");
+ prevCategory = category;
}
if (option.optionUsageRestrictions() == OptionUsageRestrictions.DOCUMENTED) {