Add keep annotation processing of @UsesReflectionToConstruct

Bug: b/392865072
Change-Id: I98d699aedd94ffaefa6786182fdb238dfdef3b02
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java
index 7104c92..88b07f1 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java
@@ -21,6 +21,7 @@
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.MethodAccess;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.Target;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.UsedByReflection;
+import com.android.tools.r8.keepanno.ast.AnnotationConstants.UsesReflectionToConstruct;
 import com.android.tools.r8.keepanno.ast.KeepAnnotationPattern;
 import com.android.tools.r8.keepanno.ast.KeepBindingReference;
 import com.android.tools.r8.keepanno.ast.KeepBindings;
@@ -29,6 +30,7 @@
 import com.android.tools.r8.keepanno.ast.KeepCheck.KeepCheckKind;
 import com.android.tools.r8.keepanno.ast.KeepClassBindingReference;
 import com.android.tools.r8.keepanno.ast.KeepClassItemPattern;
+import com.android.tools.r8.keepanno.ast.KeepClassPattern;
 import com.android.tools.r8.keepanno.ast.KeepCondition;
 import com.android.tools.r8.keepanno.ast.KeepConsequences;
 import com.android.tools.r8.keepanno.ast.KeepConstraint;
@@ -102,6 +104,7 @@
   private static boolean isEmbeddedAnnotation(String descriptor) {
     if (AnnotationConstants.Edge.isDescriptor(descriptor)
         || AnnotationConstants.UsesReflection.isDescriptor(descriptor)
+        || AnnotationConstants.UsesReflectionToConstruct.isDescriptor(descriptor)
         || AnnotationConstants.ForApi.isDescriptor(descriptor)
         || AnnotationConstants.UsedByReflection.isDescriptor(descriptor)
         || AnnotationConstants.UsedByNative.isDescriptor(descriptor)
@@ -356,6 +359,16 @@
                     .setClassNamePattern(KeepQualifiedClassNamePattern.exact(className))
                     .build());
       }
+      if (AnnotationConstants.UsesReflectionToConstruct.isDescriptor(descriptor)) {
+        return new UsesReflectionToConstructVisitor(
+            parsingContext,
+            parent::accept,
+            setContext,
+            bindingsHelper ->
+                KeepClassItemPattern.builder()
+                    .setClassNamePattern(KeepQualifiedClassNamePattern.exact(className))
+                    .build());
+      }
       if (ForApi.isDescriptor(descriptor)) {
         return new ForApiClassVisitor(parsingContext, parent::accept, setContext, className);
       }
@@ -498,6 +511,23 @@
             bindingsHelper ->
                 createMethodItemContext(className, methodName, methodDescriptor, bindingsHelper));
       }
+      if (AnnotationConstants.UsesReflectionToConstruct.isDescriptor(descriptor)) {
+        return new UsesReflectionToConstructVisitor(
+            parsingContext,
+            parent::accept,
+            setContext,
+            bindingsHelper ->
+                createMethodItemContext(className, methodName, methodDescriptor, bindingsHelper));
+      }
+      if (AnnotationConstants.UsesReflectionToConstruct.isKotlinRepeatableContainerDescriptor(
+          descriptor)) {
+        return new UsesReflectionForInstantiationContainerVisitor(
+            parsingContext,
+            parent::accept,
+            setContext,
+            bindingsHelper ->
+                createMethodItemContext(className, methodName, methodDescriptor, bindingsHelper));
+      }
       if (AnnotationConstants.ForApi.isDescriptor(descriptor)) {
         return new ForApiMemberVisitor(
             parsingContext,
@@ -1340,6 +1370,226 @@
     }
   }
 
+  private static class ParametersClassVisitor extends AnnotationVisitorBase {
+    private final ParsingContext parsingContext;
+    private final Consumer<KeepMethodParametersPattern> consumer;
+    private final KeepMethodParametersPattern.Builder builder =
+        KeepMethodParametersPattern.builder();
+
+    public ParametersClassVisitor(
+        PropertyParsingContext parsingContext, Consumer<KeepMethodParametersPattern> consumer) {
+      super(parsingContext);
+      this.parsingContext = parsingContext;
+      this.consumer = consumer;
+    }
+
+    @Override
+    public void visit(String name, Object value) {
+      assert name == null;
+      if (value instanceof String) {
+        builder.addParameterTypePattern(KeepTypePattern.fromDescriptor("L" + value + ";"));
+      } else if (value instanceof Type) {
+        builder.addParameterTypePattern(
+            KeepTypePattern.fromDescriptor(((Type) value).getDescriptor()));
+      } else {
+        super.visit(name, value);
+      }
+    }
+
+    @Override
+    public void visitEnd() {
+      consumer.accept(builder.build());
+    }
+  }
+
+  private static class ParametersClassNamesVisitor extends AnnotationVisitorBase {
+    private final ParsingContext parsingContext;
+    private final Consumer<KeepMethodParametersPattern> consumer;
+    private final KeepMethodParametersPattern.Builder builder =
+        KeepMethodParametersPattern.builder();
+
+    public ParametersClassNamesVisitor(
+        PropertyParsingContext parsingContext, Consumer<KeepMethodParametersPattern> consumer) {
+      super(parsingContext);
+      this.parsingContext = parsingContext;
+      this.consumer = consumer;
+    }
+
+    @Override
+    public void visit(String name, Object value) {
+      assert name == null;
+      if (value instanceof String) {
+        builder.addParameterTypePattern(KeepTypePattern.fromDescriptor("L" + value + ";"));
+      } else {
+        super.visit(name, value);
+      }
+    }
+
+    @Override
+    public void visitEnd() {
+      consumer.accept(builder.build());
+    }
+  }
+
+  private static class UsesReflectionForInstantiationContainerVisitor
+      extends AnnotationVisitorBase {
+
+    private final AnnotationParsingContext parsingContext;
+    private final Parent<KeepEdge> parent;
+    Consumer<KeepEdgeMetaInfo.Builder> addContext;
+    Function<UserBindingsHelper, KeepItemPattern> contextBuilder;
+
+    UsesReflectionForInstantiationContainerVisitor(
+        AnnotationParsingContext parsingContext,
+        Parent<KeepEdge> parent,
+        Consumer<KeepEdgeMetaInfo.Builder> addContext,
+        Function<UserBindingsHelper, KeepItemPattern> contextBuilder) {
+      super(parsingContext);
+      this.parsingContext = parsingContext;
+      this.parent = parent;
+      this.addContext = addContext;
+      this.contextBuilder = contextBuilder;
+    }
+
+    @Override
+    public AnnotationVisitor visitArray(String name) {
+      if (name.equals("value")) {
+        return new UsesReflectionForInstantiationsVisitor(
+            parsingContext, parent, addContext, contextBuilder);
+      }
+      return super.visitArray(name);
+    }
+
+    @Override
+    public void visitEnd() {
+      super.visitEnd();
+    }
+  }
+
+  private static class UsesReflectionForInstantiationsVisitor extends AnnotationVisitorBase {
+    private final AnnotationParsingContext parsingContext;
+    private final Parent<KeepEdge> parent;
+    Consumer<KeepEdgeMetaInfo.Builder> addContext;
+    Function<UserBindingsHelper, KeepItemPattern> contextBuilder;
+
+    public UsesReflectionForInstantiationsVisitor(
+        AnnotationParsingContext parsingContext,
+        Parent<KeepEdge> parent,
+        Consumer<KeepEdgeMetaInfo.Builder> addContext,
+        Function<UserBindingsHelper, KeepItemPattern> contextBuilder) {
+      super(parsingContext);
+      this.parsingContext = parsingContext;
+      this.parent = parent;
+      this.addContext = addContext;
+      this.contextBuilder = contextBuilder;
+    }
+
+    @Override
+    public AnnotationVisitor visitAnnotation(String name, String descriptor) {
+      assert name == null;
+      if (AnnotationConstants.UsesReflectionToConstruct.isDescriptor(descriptor)) {
+        return new UsesReflectionToConstructVisitor(
+            parsingContext, parent, addContext, contextBuilder);
+      }
+      return super.visitAnnotation(name, descriptor);
+    }
+  }
+
+  private static class UsesReflectionToConstructVisitor extends AnnotationVisitorBase {
+
+    private final ParsingContext parsingContext;
+    private final Parent<KeepEdge> parent;
+    private final KeepEdge.Builder builder = KeepEdge.builder();
+    private final KeepPreconditions.Builder preconditions = KeepPreconditions.builder();
+    private final KeepEdgeMetaInfo.Builder metaInfoBuilder = KeepEdgeMetaInfo.builder();
+    private KeepMethodParametersPattern parameters = KeepMethodParametersPattern.any();
+    private final UserBindingsHelper bindingsHelper = new UserBindingsHelper();
+
+    private KeepQualifiedClassNamePattern qualifiedName;
+
+    UsesReflectionToConstructVisitor(
+        AnnotationParsingContext parsingContext,
+        Parent<KeepEdge> parent,
+        Consumer<KeepEdgeMetaInfo.Builder> addContext,
+        Function<UserBindingsHelper, KeepItemPattern> contextBuilder) {
+      super(parsingContext);
+      this.parsingContext = parsingContext;
+      this.parent = parent;
+      KeepItemPattern context = contextBuilder.apply(bindingsHelper);
+      KeepBindingReference contextBinding =
+          bindingsHelper.defineFreshItemBinding("CONTEXT", context);
+      preconditions.addCondition(KeepCondition.builder().setItemReference(contextBinding).build());
+      addContext.accept(metaInfoBuilder);
+    }
+
+    @Override
+    public void visit(String name, Object value) {
+      if (name.equals(UsesReflectionToConstruct.classConstant) && value instanceof Type) {
+        qualifiedName =
+            KeepQualifiedClassNamePattern.exactFromDescriptor(((Type) value).getDescriptor());
+        return;
+      }
+      if (name.equals(AnnotationConstants.UsesReflectionToConstruct.className)
+          && value instanceof String) {
+        qualifiedName = KeepQualifiedClassNamePattern.exact((String) value);
+        return;
+      }
+      super.visit(name, value);
+    }
+
+    @Override
+    public AnnotationVisitor visitArray(String name) {
+      PropertyParsingContext propertyParsingContext = parsingContext.property(name);
+      if (name.equals(UsesReflectionToConstruct.params)) {
+        return new ParametersClassVisitor(
+            propertyParsingContext, parameters -> this.parameters = parameters);
+      }
+      if (name.equals(UsesReflectionToConstruct.paramClassNames)) {
+        return new ParametersClassNamesVisitor(
+            propertyParsingContext, parameters -> this.parameters = parameters);
+      }
+      return super.visitArray(name);
+    }
+
+    @Override
+    public void visitEnd() {
+      KeepClassItemPattern classItemPattern =
+          KeepClassItemPattern.builder()
+              .setClassPattern(
+                  KeepClassPattern.builder().setClassNamePattern(qualifiedName).build())
+              .build();
+      KeepClassBindingReference classBinding =
+          bindingsHelper.defineFreshClassBinding(classItemPattern);
+      KeepMemberItemPattern keepMemberItemPattern =
+          KeepMemberItemPattern.builder()
+              .setClassReference(classBinding)
+              .setMemberPattern(
+                  KeepMethodPattern.builder()
+                      .setNamePattern(KeepMethodNamePattern.instanceInitializer())
+                      .setParametersPattern(parameters)
+                      .setReturnTypeVoid()
+                      .build())
+              .build();
+      KeepMemberBindingReference memberBinding =
+          bindingsHelper.defineFreshMemberBinding(keepMemberItemPattern);
+      builder.setConsequences(
+          KeepConsequences.builder()
+              .addTarget(
+                  KeepTarget.builder()
+                      .setItemReference(classBinding)
+                      .setItemReference(memberBinding)
+                      .build())
+              .addTarget(KeepTarget.builder().setItemReference(classBinding).build())
+              .build());
+      parent.accept(
+          builder
+              .setMetaInfo(metaInfoBuilder.build())
+              .setBindings(bindingsHelper.build())
+              .setPreconditions(preconditions.build())
+              .build());
+    }
+  }
+
   private static class KeepBindingsVisitor extends AnnotationVisitorBase {
     private final ParsingContext parsingContext;
     private final UserBindingsHelper helper;
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestBase.java b/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestBase.java
index 45b8aa6..515c37d 100644
--- a/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestBase.java
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestBase.java
@@ -7,13 +7,17 @@
 import static com.android.tools.r8.R8TestBuilder.KeepAnnotationLibrary.ANDROIDX;
 import static com.android.tools.r8.R8TestBuilder.KeepAnnotationLibrary.LEGACY;
 
+import com.android.tools.r8.KotlinCompileMemoizer;
 import com.android.tools.r8.R8TestBuilder.KeepAnnotationLibrary;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.keepanno.KeepAnnoParameters.KeepAnnoConfig;
 import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Path;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.List;
 
 public abstract class KeepAnnoTestBase extends TestBase {
@@ -44,4 +48,17 @@
   public KeepAnnoTestBuilder testForKeepAnnoAndroidX(KeepAnnoParameters params) throws IOException {
     return testForKeepAnno(params, ANDROIDX);
   }
+
+  protected static KotlinCompileMemoizer getCompileMemoizerWithKeepAnnoLib(
+      Collection<Path> sources) {
+    assert sources.size() > 0;
+    Path keepAnnoLib;
+    try {
+      keepAnnoLib = KeepAnnoTestUtils.getKeepAnnoLib(getStaticTemp());
+    } catch (IOException e) {
+      throw new UncheckedIOException(e);
+    }
+    return new KotlinCompileMemoizer(sources)
+        .configure(kotlinCompilerTool -> kotlinCompilerTool.addClasspathFiles(keepAnnoLib));
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestBuilder.java b/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestBuilder.java
index b9baff4..1f9fb46 100644
--- a/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestBuilder.java
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestBuilder.java
@@ -5,6 +5,9 @@
 package com.android.tools.r8.keepanno;
 
 import static com.android.tools.r8.R8TestBuilder.KeepAnnotationLibrary.ANDROIDX;
+import static com.android.tools.r8.utils.FileUtils.isClassFile;
+import static com.android.tools.r8.utils.FileUtils.isJarFile;
+import static com.android.tools.r8.utils.FileUtils.isZipFile;
 
 import com.android.tools.r8.ExternalR8TestBuilder;
 import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
@@ -33,6 +36,8 @@
 import com.android.tools.r8.keepanno.proto.KeepSpecProtos.KeepSpec;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.partial.R8PartialCompilationConfiguration.Builder;
+import com.android.tools.r8.utils.ZipUtils;
+import com.google.common.io.ByteStreams;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -100,6 +105,9 @@
   public abstract KeepAnnoTestBuilder addProgramClassFileData(List<byte[]> programClasses)
       throws IOException;
 
+  public abstract KeepAnnoTestBuilder addProgramFilesWithoutAnnotations(List<Path> programFiles)
+      throws IOException;
+
   public final KeepAnnoTestBuilder addKeepMainRule(Class<?> mainClass) {
     return applyIfShrinker(b -> b.addKeepMainRule(mainClass));
   }
@@ -108,8 +116,16 @@
     return applyIfShrinker(b -> b.addKeepClassRules(classes));
   }
 
+  public final KeepAnnoTestBuilder addKeepRules(String... classes) {
+    return applyIfShrinker(b -> b.addKeepRules(classes));
+  }
+
+  public abstract void compile() throws Exception;
+
   public abstract SingleTestRunResult<?> run(Class<?> mainClass) throws Exception;
 
+  public abstract SingleTestRunResult<?> run(String mainClass) throws Exception;
+
   public KeepAnnoTestBuilder applyIfShrinker(
       ThrowableConsumer<TestShrinkerBuilder<?, ?, ?, ?, ?>> builderConsumer) {
     applyIfR8(builderConsumer);
@@ -132,6 +148,12 @@
     return this;
   }
 
+  public KeepAnnoTestBuilder applyIfR8OrR8Partial(
+      ThrowableConsumer<TestShrinkerBuilder<?, ?, ?, ?, ?>> r8BuilderConsumer,
+      ThrowableConsumer<R8PartialTestBuilder> r8PartialBuilderConsumer) {
+    return this;
+  }
+
   public KeepAnnoTestBuilder applyIfPG(ThrowableConsumer<ProguardTestBuilder> builderConsumer) {
     return this;
   }
@@ -213,6 +235,14 @@
     }
 
     @Override
+    public KeepAnnoTestBuilder addProgramFilesWithoutAnnotations(List<Path> programFiles)
+        throws IOException {
+      // TODO(b/392865072): Ensure annotations are not processed.
+      builder.addProgramFiles(programFiles);
+      return this;
+    }
+
+    @Override
     public KeepAnnoTestBuilder inspectOutputRules(Consumer<String> configConsumer) {
       // Ignore the consumer.
       return this;
@@ -225,9 +255,19 @@
     }
 
     @Override
+    public void compile() throws Exception {
+      // Do nothing.
+    }
+
+    @Override
     public SingleTestRunResult<?> run(Class<?> mainClass) throws Exception {
       return builder.run(parameters().getRuntime(), mainClass);
     }
+
+    @Override
+    public SingleTestRunResult<?> run(String mainClass) throws Exception {
+      return builder.run(parameters().getRuntime(), mainClass);
+    }
   }
 
   private abstract static class R8NativeBuilderBase<
@@ -275,9 +315,29 @@
     }
 
     @Override
+    public KeepAnnoTestBuilder applyIfR8OrR8Partial(
+        ThrowableConsumer<TestShrinkerBuilder<?, ?, ?, ?, ?>> r8BuilderConsumer,
+        ThrowableConsumer<R8PartialTestBuilder> r8PartialBuilderConsumer) {
+      r8BuilderConsumer.acceptWithRuntimeException(builder);
+      return this;
+    }
+
+    @Override
     public KeepAnnoTestBuilder addProgramFiles(List<Path> programFiles) throws IOException {
       for (Path programFile : programFiles) {
-        extractAndAdd(Files.readAllBytes(programFile));
+        if (isClassFile(programFile)) {
+          extractAndAdd(Files.readAllBytes(programFile));
+        } else if (isJarFile(programFile) || isZipFile(programFile)) {
+          ZipUtils.iter(
+              programFile,
+              (entry, input) -> {
+                if (isClassFile(entry.getName())) {
+                  extractAndAdd(ByteStreams.toByteArray(input));
+                }
+              });
+        } else {
+          assert false : "Unsupported file format";
+        }
       }
       return this;
     }
@@ -331,6 +391,14 @@
     }
 
     @Override
+    public KeepAnnoTestBuilder addProgramFilesWithoutAnnotations(List<Path> programFiles)
+        throws IOException {
+      // TODO(b/392865072): Ensure annotations are not processed.
+      builder.addProgramFiles(programFiles);
+      return this;
+    }
+
+    @Override
     public KeepAnnoTestBuilder inspectOutputRules(Consumer<String> configConsumer) {
       compileResultConsumers.add(
           result -> configConsumer.accept(result.getProguardConfiguration()));
@@ -344,11 +412,24 @@
     }
 
     @Override
+    public void compile() throws Exception {
+      R compileResult = builder.compile();
+      compileResultConsumers.forEach(fn -> fn.accept(compileResult));
+    }
+
+    @Override
     public SingleTestRunResult<?> run(Class<?> mainClass) throws Exception {
       R compileResult = builder.compile();
       compileResultConsumers.forEach(fn -> fn.accept(compileResult));
       return compileResult.run(parameters().getRuntime(), mainClass);
     }
+
+    @Override
+    public SingleTestRunResult<?> run(String mainClass) throws Exception {
+      R compileResult = builder.compile();
+      compileResultConsumers.forEach(fn -> fn.accept(compileResult));
+      return compileResult.run(parameters().getRuntime(), mainClass);
+    }
   }
 
   private static class R8NativeBuilder
@@ -410,6 +491,14 @@
     }
 
     @Override
+    public KeepAnnoTestBuilder applyIfR8OrR8Partial(
+        ThrowableConsumer<TestShrinkerBuilder<?, ?, ?, ?, ?>> r8BuilderConsumer,
+        ThrowableConsumer<R8PartialTestBuilder> r8PartialBuilderConsumer) {
+      r8PartialBuilderConsumer.acceptWithRuntimeException(builder);
+      return this;
+    }
+
+    @Override
     boolean isExtractRules() {
       return config == KeepAnnoConfig.R8_PARTIAL_RULES;
     }
@@ -455,6 +544,14 @@
     }
 
     @Override
+    public KeepAnnoTestBuilder applyIfR8OrR8Partial(
+        ThrowableConsumer<TestShrinkerBuilder<?, ?, ?, ?, ?>> r8BuilderConsumer,
+        ThrowableConsumer<R8PartialTestBuilder> r8PartialBuilderConsumer) {
+      r8BuilderConsumer.acceptWithRuntimeException(builder);
+      return this;
+    }
+
+    @Override
     public KeepAnnoTestBuilder addProgramFiles(List<Path> programFiles) throws IOException {
       List<String> rules = KeepAnnoTestUtils.extractRulesFromFiles(programFiles, extractorOptions);
       builder.addProgramFiles(programFiles);
@@ -484,6 +581,14 @@
     }
 
     @Override
+    public KeepAnnoTestBuilder addProgramFilesWithoutAnnotations(List<Path> programFiles)
+        throws IOException {
+      // TODO(b/392865072): Ensure annotations are not processed.
+      builder.addProgramFiles(programFiles);
+      return this;
+    }
+
+    @Override
     public KeepAnnoTestBuilder inspectOutputRules(Consumer<String> configConsumer) {
       configConsumers.add(lines -> configConsumer.accept(String.join("\n", lines)));
       return this;
@@ -495,11 +600,23 @@
     }
 
     @Override
+    public void compile() throws Exception {
+      builder.compile();
+    }
+
+    @Override
     public SingleTestRunResult<?> run(Class<?> mainClass) throws Exception {
       configConsumers.forEach(fn -> fn.accept(builder.getConfig()));
       extractedRulesConsumers.forEach(fn -> fn.accept(extractedRules));
       return builder.run(parameters().getRuntime(), mainClass);
     }
+
+    @Override
+    public SingleTestRunResult<?> run(String mainClass) throws Exception {
+      configConsumers.forEach(fn -> fn.accept(builder.getConfig()));
+      extractedRulesConsumers.forEach(fn -> fn.accept(extractedRules));
+      return builder.run(parameters().getRuntime(), mainClass);
+    }
   }
 
   private static class PGBuilder extends KeepAnnoTestBuilder {
@@ -521,11 +638,7 @@
       builder =
           TestBase.testForProguard(KeepAnnoTestUtils.PG_VERSION, temp)
               .applyIf(
-                  keepAnnotationLibrary == ANDROIDX,
-                  b ->
-                      b.addDefaultRuntimeLibrary(parameters())
-                          .addLibraryFiles(
-                              kotlinc.getKotlinStdlibJar(), kotlinc.getKotlinAnnotationJar()))
+                  keepAnnotationLibrary == ANDROIDX, b -> b.addDefaultRuntimeLibrary(parameters()))
               .addProgramFiles(KeepAnnoTestUtils.getKeepAnnoLib(temp, keepAnnotationLibrary))
               .setMinApi(parameters());
     }
@@ -566,6 +679,14 @@
     }
 
     @Override
+    public KeepAnnoTestBuilder addProgramFilesWithoutAnnotations(List<Path> programFiles)
+        throws IOException {
+      // TODO(b/392865072): Ensure annotations are not processed.
+      builder.addProgramFiles(programFiles);
+      return this;
+    }
+
+    @Override
     public KeepAnnoTestBuilder inspectOutputRules(Consumer<String> configConsumer) {
       configConsumers.add(lines -> configConsumer.accept(String.join("\n", lines)));
       return this;
@@ -577,10 +698,22 @@
     }
 
     @Override
+    public void compile() throws Exception {
+      builder.compile();
+    }
+
+    @Override
     public SingleTestRunResult<?> run(Class<?> mainClass) throws Exception {
       configConsumers.forEach(fn -> fn.accept(builder.getConfig()));
       extractedRulesConsumers.forEach(fn -> fn.accept(extractedRules));
       return builder.run(parameters().getRuntime(), mainClass);
     }
+
+    @Override
+    public SingleTestRunResult<?> run(String mainClass) throws Exception {
+      configConsumers.forEach(fn -> fn.accept(builder.getConfig()));
+      extractedRulesConsumers.forEach(fn -> fn.accept(extractedRules));
+      return builder.run(parameters().getRuntime(), mainClass);
+    }
   }
 }
diff --git a/src/test/java/com/android/tools/r8/keepanno/androidx/KeepAnnoTestExtractedRulesBase.java b/src/test/java/com/android/tools/r8/keepanno/androidx/KeepAnnoTestExtractedRulesBase.java
new file mode 100644
index 0000000..c652577
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/androidx/KeepAnnoTestExtractedRulesBase.java
@@ -0,0 +1,201 @@
+// Copyright (c) 2025, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.keepanno.androidx;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assume.assumeFalse;
+
+import com.android.tools.r8.KotlinCompileMemoizer;
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion;
+import com.android.tools.r8.KotlinTestParameters;
+import com.android.tools.r8.R8TestBuilder;
+import com.android.tools.r8.Version;
+import com.android.tools.r8.keepanno.KeepAnnoParameters;
+import com.android.tools.r8.keepanno.KeepAnnoTestBase;
+import com.android.tools.r8.utils.SemanticVersion;
+import com.google.common.collect.ImmutableList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.junit.runners.Parameterized.Parameter;
+
+public abstract class KeepAnnoTestExtractedRulesBase extends KeepAnnoTestBase {
+  @Parameter(0)
+  public KeepAnnoParameters parameters;
+
+  @Parameter(1)
+  public KotlinTestParameters kotlinParameters;
+
+  protected static KotlinCompileMemoizer compilationResults;
+  protected static KotlinCompileMemoizer compilationResultsClassName;
+
+  protected abstract String getExpectedOutput();
+
+  protected static List<String> trimRules(List<String> rules) {
+    List<String> trimmedRules =
+        rules.stream()
+            .flatMap(s -> Arrays.stream(s.split("\n")))
+            .filter(rule -> !rule.startsWith("#"))
+            .collect(Collectors.toList());
+    return trimmedRules;
+  }
+
+  public static class ExpectedRule {
+    private final String conditionClass;
+    private final String conditionMembers;
+    private final String consequentClass;
+    private final String consequentMembers;
+
+    private ExpectedRule(Builder builder) {
+      this.conditionClass = builder.conditionClass;
+      this.conditionMembers = builder.conditionMembers;
+      this.consequentClass = builder.consequentClass;
+      this.consequentMembers = builder.consequentMembers;
+    }
+
+    public String getRule(boolean r8) {
+      return "-if class "
+          + conditionClass
+          + " "
+          + conditionMembers
+          + " -keepclasseswithmembers"
+          + (r8 ? ",allowaccessmodification" : "")
+          + " class "
+          + consequentClass
+          + " "
+          + consequentMembers;
+    }
+
+    public static Builder builder() {
+      return new Builder();
+    }
+
+    public static class Builder {
+      private String conditionClass;
+      private String conditionMembers;
+      private String consequentClass;
+      private String consequentMembers;
+
+      private Builder() {}
+
+      public Builder setConditionClass(Class<?> conditionClass) {
+        this.conditionClass = conditionClass.getTypeName();
+        return this;
+      }
+
+      public Builder setConditionClass(String conditionClass) {
+        this.conditionClass = conditionClass;
+        return this;
+      }
+
+      public Builder setConditionMembers(String conditionMembers) {
+        this.conditionMembers = conditionMembers;
+        return this;
+      }
+
+      public Builder setConsequentClass(Class<?> consequentClass) {
+        this.consequentClass = consequentClass.getTypeName();
+        return this;
+      }
+
+      public Builder setConsequentClass(String consequentClass) {
+        this.consequentClass = consequentClass;
+        return this;
+      }
+
+      public Builder setConsequentMembers(String consequentMembers) {
+        this.consequentMembers = consequentMembers;
+        return this;
+      }
+
+      public ExpectedRule build() {
+        return new ExpectedRule(this);
+      }
+    }
+  }
+
+  protected void runTestExtractedRulesJava(List<Class<?>> testClasses, ExpectedRule expectedRule)
+      throws Exception {
+    Class<?> mainClass = testClasses.iterator().next();
+    testForKeepAnnoAndroidX(parameters)
+        .applyIfPG(
+            b -> {
+              KotlinCompiler kotlinc =
+                  new KotlinCompiler(KotlinCompilerVersion.MAX_SUPPORTED_VERSION);
+              b.addLibraryFiles(kotlinc.getKotlinStdlibJar(), kotlinc.getKotlinAnnotationJar());
+            })
+        .addProgramClasses(testClasses)
+        .addKeepMainRule(mainClass)
+        .setExcludedOuterClass(getClass())
+        .inspectExtractedRules(
+            rules -> {
+              if (parameters.isExtractRules()) {
+                assertEquals(
+                    ImmutableList.of(expectedRule.getRule(parameters.isR8())), trimRules(rules));
+              }
+            })
+        .run(mainClass)
+        .assertSuccessWithOutput(getExpectedOutput());
+  }
+
+  protected void runTestExtractedRulesKotlin(
+      KotlinCompileMemoizer compilation, String mainClass, ExpectedRule expectedRule)
+      throws Exception {
+    // TODO(b/392865072): Legacy R8 fails with AssertionError: Synthetic class kinds should agree.
+    assumeFalse(parameters.isLegacyR8());
+    // TODO(b/392865072): Reference fails with AssertionError: Built-in class kotlin.Any is not
+    // found (in kotlin.reflect code).
+    assumeFalse(parameters.isReference());
+    // TODO(b/392865072): Proguard 7.4.1 fails with "Encountered corrupt @kotlin/Metadata for class
+    // <binary name> (version 2.1.0)".
+    assumeFalse(parameters.isPG());
+    testForKeepAnnoAndroidX(parameters)
+        .addProgramFiles(ImmutableList.of(compilation.getForConfiguration(kotlinParameters)))
+        .addProgramFilesWithoutAnnotations(
+            ImmutableList.of(
+                kotlinParameters.getCompiler().getKotlinStdlibJar(),
+                kotlinParameters.getCompiler().getKotlinReflectJar(),
+                kotlinParameters.getCompiler().getKotlinAnnotationJar()))
+        .applyIfR8(
+            b ->
+                b.applyIf(
+                    b instanceof R8TestBuilder && Version.isMainVersion(),
+                    bb ->
+                        ((R8TestBuilder<?, ?, ?>) bb)
+                            .setFakeCompilerVersion(SemanticVersion.max())
+                            .allowDiagnosticMessages()))
+        .addKeepRules("-keepattributes RuntimeVisibleAnnotations")
+        .addKeepRules("-keep class kotlin.Metadata { *; }")
+        .addKeepRules(
+            "-keep class " + mainClass + " { public static void main(java.lang.String[]); }")
+        .applyIfR8OrR8Partial(
+            b ->
+                b.addOptionsModification(
+                    options -> {
+                      options.testing.allowedUnusedDontWarnPatterns.add(
+                          "kotlin.reflect.jvm.internal.**");
+                      options.testing.allowedUnusedDontWarnPatterns.add("java.lang.ClassValue");
+                    }),
+            b ->
+                b.addR8PartialR8OptionsModification(
+                    options -> {
+                      options.testing.allowedUnusedDontWarnPatterns.add(
+                          "kotlin.reflect.jvm.internal.**");
+                      options.testing.allowedUnusedDontWarnPatterns.add("java.lang.ClassValue");
+                    }))
+        .inspectExtractedRules(
+            rules -> {
+              if (parameters.isExtractRules()) {
+                assertEquals(
+                    ImmutableList.of(expectedRule.getRule(parameters.isR8())), trimRules(rules));
+              }
+            })
+        .run(mainClass)
+        .applyIf(
+            parameters.isExtractRules(),
+            b -> b.assertSuccessWithOutput(getExpectedOutput()),
+            b -> b.assertSuccessWithOutput(""));
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/androidx/KeepUsesReflectionForInstantiationNoArgsConstructorTest.java b/src/test/java/com/android/tools/r8/keepanno/androidx/KeepUsesReflectionForInstantiationNoArgsConstructorTest.java
new file mode 100644
index 0000000..dfa7b78
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/androidx/KeepUsesReflectionForInstantiationNoArgsConstructorTest.java
@@ -0,0 +1,166 @@
+// Copyright (c) 2025, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.keepanno.androidx;
+
+import static com.android.tools.r8.ToolHelper.getFilesInTestFolderRelativeToClass;
+import static org.junit.Assert.assertEquals;
+
+import androidx.annotation.keep.UsesReflectionToConstruct;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.ImmutableList;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Collection;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class KeepUsesReflectionForInstantiationNoArgsConstructorTest
+    extends KeepAnnoTestExtractedRulesBase {
+
+  // String constant to be references from annotations.
+  static final String classNameOfKeptClass =
+      "com.android.tools.r8.keepanno.androidx.KeepUsesReflectionForInstantiationNoArgsConstructorTest$KeptClass";
+
+  @Parameterized.Parameters(name = "{0}, {1}")
+  public static Collection<Object[]> data() {
+    assertEquals(KeptClass.class.getTypeName(), classNameOfKeptClass);
+    return buildParameters(
+        createParameters(getTestParameters().withDefaultRuntimes().withMaximumApiLevel().build()),
+        getKotlinTestParameters().withLatestCompiler().build());
+  }
+
+  protected String getExpectedOutput() {
+    return StringUtils.lines("<init>()");
+  }
+
+  private static Collection<Path> getKotlinSources() {
+    try {
+      return getFilesInTestFolderRelativeToClass(
+          KeepUsesReflectionForInstantiationNoArgsConstructorTest.class,
+          "kt",
+          "OnlyNoArgsConstructor.kt",
+          "KeptClass.kt");
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  private static Collection<Path> getKotlinSourcesClassName() {
+    try {
+      return getFilesInTestFolderRelativeToClass(
+          KeepUsesReflectionForInstantiationNoArgsConstructorTest.class,
+          "kt",
+          "OnlyNoArgsConstructorClassName.kt",
+          "KeptClass.kt");
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @BeforeClass
+  public static void beforeClass() throws Exception {
+    compilationResults = getCompileMemoizerWithKeepAnnoLib(getKotlinSources());
+    compilationResultsClassName = getCompileMemoizerWithKeepAnnoLib(getKotlinSourcesClassName());
+  }
+
+  @Test
+  public void testOnlyNoArgsConstructor() throws Exception {
+    runTestExtractedRulesJava(
+        ImmutableList.of(OnlyNoArgsConstructor.class, KeptClass.class),
+        ExpectedRule.builder()
+            .setConditionClass(OnlyNoArgsConstructor.class)
+            .setConditionMembers("{ void foo(java.lang.Class); }")
+            .setConsequentClass(KeptClass.class)
+            .setConsequentMembers("{ void <init>(); }")
+            .build());
+  }
+
+  static class OnlyNoArgsConstructor {
+
+    @UsesReflectionToConstruct(
+        className = classNameOfKeptClass,
+        params = {})
+    public void foo(Class<KeptClass> clazz) throws Exception {
+      if (clazz != null) {
+        clazz.getDeclaredConstructor().newInstance();
+      }
+    }
+
+    public static void main(String[] args) throws Exception {
+      new OnlyNoArgsConstructor().foo(System.nanoTime() > 0 ? KeptClass.class : null);
+    }
+  }
+
+  @Test
+  public void testOnlyNoArgsConstructorClassNames() throws Exception {
+    runTestExtractedRulesJava(
+        ImmutableList.of(OnlyNoArgsConstructorClassNames.class, KeptClass.class),
+        ExpectedRule.builder()
+            .setConditionClass(OnlyNoArgsConstructorClassNames.class)
+            .setConditionMembers("{ void foo(java.lang.Class); }")
+            .setConsequentClass(KeptClass.class)
+            .setConsequentMembers("{ void <init>(); }")
+            .build());
+  }
+
+  static class OnlyNoArgsConstructorClassNames {
+
+    @UsesReflectionToConstruct(
+        className = classNameOfKeptClass,
+        params = {})
+    public void foo(Class<KeptClass> clazz) throws Exception {
+      if (clazz != null) {
+        clazz.getDeclaredConstructor().newInstance();
+      }
+    }
+
+    public static void main(String[] args) throws Exception {
+      new OnlyNoArgsConstructorClassNames().foo(System.nanoTime() > 0 ? KeptClass.class : null);
+    }
+  }
+
+  @Test
+  public void testOnlyNoArgsConstructorKotlin() throws Exception {
+    runTestExtractedRulesKotlin(
+        compilationResults,
+        "com.android.tools.r8.keepanno.androidx.kt.OnlyNoArgsConstructorKt",
+        ExpectedRule.builder()
+            .setConditionClass("com.android.tools.r8.keepanno.androidx.kt.OnlyNoArgsConstructor")
+            .setConditionMembers("{ void foo(kotlin.reflect.KClass); }")
+            .setConsequentClass("com.android.tools.r8.keepanno.androidx.kt.KeptClass")
+            .setConsequentMembers("{ void <init>(); }")
+            .build());
+  }
+
+  @Test
+  public void testOnlyNoArgsConstructorKotlinClassName() throws Exception {
+    runTestExtractedRulesKotlin(
+        compilationResultsClassName,
+        "com.android.tools.r8.keepanno.androidx.kt.OnlyNoArgsConstructorClassNameKt",
+        ExpectedRule.builder()
+            .setConditionClass(
+                "com.android.tools.r8.keepanno.androidx.kt.OnlyNoArgsConstructorClassName")
+            .setConditionMembers("{ void foo(kotlin.reflect.KClass); }")
+            .setConsequentClass("com.android.tools.r8.keepanno.androidx.kt.KeptClass")
+            .setConsequentMembers("{ void <init>(); }")
+            .build());
+  }
+
+  static class KeptClass {
+    KeptClass() {
+      System.out.println("<init>()");
+    }
+
+    KeptClass(int i) {
+      System.out.println("<init>(int)");
+    }
+
+    KeptClass(long j) {
+      System.out.println("<init>(long)");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/androidx/kt/KeptClass.kt b/src/test/java/com/android/tools/r8/keepanno/androidx/kt/KeptClass.kt
new file mode 100644
index 0000000..fb55316
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/androidx/kt/KeptClass.kt
@@ -0,0 +1,15 @@
+package com.android.tools.r8.keepanno.androidx.kt
+
+class KeptClass() {
+  init {
+    println("<init>()")
+  }
+
+  constructor(i: Int) : this() {
+    println("<init>(Int)")
+  }
+
+  constructor(l: Long) : this() {
+    println("<init>(Long)")
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/androidx/kt/OnlyNoArgsConstructor.kt b/src/test/java/com/android/tools/r8/keepanno/androidx/kt/OnlyNoArgsConstructor.kt
new file mode 100644
index 0000000..4dd03cc
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/androidx/kt/OnlyNoArgsConstructor.kt
@@ -0,0 +1,16 @@
+package com.android.tools.r8.keepanno.androidx.kt
+
+import androidx.annotation.keep.UsesReflectionToConstruct
+import kotlin.reflect.KClass
+import kotlin.reflect.full.primaryConstructor
+
+class OnlyNoArgsConstructor {
+  @UsesReflectionToConstruct(classConstant = KeptClass::class, params = [])
+  fun foo(clazz: KClass<KeptClass>?) {
+    clazz?.primaryConstructor?.call()
+  }
+}
+
+fun main() {
+  OnlyNoArgsConstructor().foo(if (System.nanoTime() > 0) KeptClass::class else null)
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/androidx/kt/OnlyNoArgsConstructorClassName.kt b/src/test/java/com/android/tools/r8/keepanno/androidx/kt/OnlyNoArgsConstructorClassName.kt
new file mode 100644
index 0000000..8652223
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/androidx/kt/OnlyNoArgsConstructorClassName.kt
@@ -0,0 +1,19 @@
+package com.android.tools.r8.keepanno.androidx.kt
+
+import androidx.annotation.keep.UsesReflectionToConstruct
+import kotlin.reflect.KClass
+import kotlin.reflect.full.primaryConstructor
+
+class OnlyNoArgsConstructorClassName {
+  @UsesReflectionToConstruct(
+    className = "com.android.tools.r8.keepanno.androidx.kt.KeptClass",
+    params = [],
+  )
+  fun foo(clazz: KClass<KeptClass>?) {
+    clazz?.primaryConstructor?.call()
+  }
+}
+
+fun main() {
+  OnlyNoArgsConstructorClassName().foo(if (System.nanoTime() > 0) KeptClass::class else null)
+}
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/KotlinMetadataTestBase.java b/src/test/java/com/android/tools/r8/kotlin/metadata/KotlinMetadataTestBase.java
index c7a31d0..366f7b2 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/KotlinMetadataTestBase.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/KotlinMetadataTestBase.java
@@ -7,6 +7,8 @@
 import static com.android.tools.r8.utils.FunctionUtils.ignoreArgument;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static junit.framework.TestCase.assertNotNull;
+import static org.hamcrest.CoreMatchers.anyOf;
+import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
@@ -174,7 +176,9 @@
   public static void verifyExpectedWarningsFromKotlinReflectAndStdLib(
       TestCompileResult<?, ?> compileResult) {
     compileResult.assertAllWarningMessagesMatch(
-        equalTo("Resource 'META-INF/versions/9/module-info.class' already exists."));
+        anyOf(
+            equalTo("Resource 'META-INF/versions/9/module-info.class' already exists."),
+            containsString("-upto-")));
   }
 
   protected String unresolvedReferenceMessage(KotlinTestParameters param, String ref) {
diff --git a/src/test/testbase/java/com/android/tools/r8/ToolHelper.java b/src/test/testbase/java/com/android/tools/r8/ToolHelper.java
index a90871d..547024a 100644
--- a/src/test/testbase/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/testbase/java/com/android/tools/r8/ToolHelper.java
@@ -2712,12 +2712,16 @@
   }
 
   public static Collection<Path> getFilesInTestFolderRelativeToClass(
-      Class<?> clazz, String folderName, String endsWith) throws IOException {
+      Class<?> clazz, String folderName, String... endsWith) throws IOException {
     Path subFolder = getTestFolderForClass(clazz).resolve(folderName);
     assert Files.isDirectory(subFolder);
-    try (Stream<Path> walker = Files.walk(subFolder)) {
-      return walker.filter(path -> path.toString().endsWith(endsWith)).collect(Collectors.toList());
+    List<Path> result = new ArrayList<>();
+    for (String s : endsWith) {
+      try (Stream<Path> walker = Files.walk(subFolder)) {
+        walker.filter(path -> path.toString().endsWith(s)).forEach(result::add);
+      }
     }
+    return result;
   }
 
   /** This code only works if run with depot_tools on the path */
diff --git a/src/test/testbase/java/com/android/tools/r8/keepanno/KeepAnnoTestUtils.java b/src/test/testbase/java/com/android/tools/r8/keepanno/KeepAnnoTestUtils.java
index 53f0924..36968f6 100644
--- a/src/test/testbase/java/com/android/tools/r8/keepanno/KeepAnnoTestUtils.java
+++ b/src/test/testbase/java/com/android/tools/r8/keepanno/KeepAnnoTestUtils.java
@@ -5,6 +5,10 @@
 package com.android.tools.r8.keepanno;
 
 import static com.android.tools.r8.R8TestBuilder.KeepAnnotationLibrary.ANDROIDX;
+import static com.android.tools.r8.utils.FileUtils.isClassFile;
+import static com.android.tools.r8.utils.FileUtils.isJarFile;
+import static com.android.tools.r8.utils.FileUtils.isZipFile;
+import static com.android.tools.r8.utils.ZipUtils.isClassFile;
 import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.ByteDataView;
@@ -18,6 +22,9 @@
 import com.android.tools.r8.keepanno.keeprules.KeepRuleExtractorOptions;
 import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.ZipUtils;
+import com.google.common.collect.ImmutableList;
+import com.google.common.io.ByteStreams;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -53,7 +60,7 @@
         try (Stream<Path> paths = Files.list(annoDir)) {
           paths.forEach(
               p -> {
-                if (FileUtils.isClassFile(p)) {
+                if (isClassFile(p)) {
                   byte[] data = FileUtils.uncheckedReadAllBytes(p);
                   String fileName = p.getFileName().toString();
                   String className = fileName.substring(0, fileName.lastIndexOf('.'));
@@ -74,17 +81,31 @@
 
   public static List<String> extractRulesFromFiles(
       List<Path> inputFiles, KeepRuleExtractorOptions extractorOptions) {
-    return extractRulesFromBytes(
-        ListUtils.map(
-            inputFiles,
-            path -> {
-              try {
-                return Files.readAllBytes(path);
-              } catch (IOException e) {
-                throw new RuntimeException(e);
-              }
-            }),
-        extractorOptions);
+    List<String> result = new ArrayList<>();
+    for (Path inputFile : inputFiles) {
+      try {
+        if (isClassFile(inputFile)) {
+          result.addAll(
+              extractRulesFromBytes(
+                  ImmutableList.of(Files.readAllBytes(inputFile)), extractorOptions));
+        } else if (isJarFile(inputFile) || isZipFile(inputFile)) {
+          List<byte[]> classFilesFromArchive = new ArrayList<>();
+          ZipUtils.iter(
+              inputFile,
+              (entry, input) -> {
+                if (ZipUtils.isClassFile(entry.getName())) {
+                  classFilesFromArchive.add(ByteStreams.toByteArray(input));
+                }
+              });
+          result.addAll(extractRulesFromBytes(classFilesFromArchive, extractorOptions));
+        } else {
+          assert false : "Unsupported file format";
+        }
+      } catch (IOException e) {
+        throw new RuntimeException(e);
+      }
+    }
+    return result;
   }
 
   public static List<String> extractRules(