Fix resolver NPE for no-binary artifacts (#1595)
diff --git a/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/lockfile/BUILD b/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/lockfile/BUILD
index 53dc9ba..cd78c91 100644
--- a/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/lockfile/BUILD
+++ b/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/lockfile/BUILD
@@ -6,6 +6,7 @@
     srcs = glob(["*.java"]),
     visibility = [
         "//private/tools/java/com/github/bazelbuild/rules_jvm_external:__subpackages__",
+        "//tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle:__pkg__",
         "//tests/com/github/bazelbuild/rules_jvm_external/resolver/lockfile:__pkg__",
     ],
     deps = [
diff --git a/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/lockfile/V3LockFile.java b/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/lockfile/V3LockFile.java
index 8a4cc93..d200069 100644
--- a/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/lockfile/V3LockFile.java
+++ b/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/lockfile/V3LockFile.java
@@ -231,7 +231,11 @@
           @SuppressWarnings("unchecked")
           Map<String, String> shasums =
               (Map<String, String>) artifactValue.computeIfAbsent("shasums", k -> new TreeMap<>());
-          info.getSha256().ifPresent(sha -> shasums.put(classifier, sha));
+          if (info.getSha256().isPresent()) {
+            shasums.put(classifier, info.getSha256().get());
+          } else {
+            shasums.putIfAbsent(classifier, null);
+          }
 
           info.getRepositories()
               .forEach(
diff --git a/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/remote/Downloader.java b/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/remote/Downloader.java
index c5aa48e..5e707a2 100644
--- a/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/remote/Downloader.java
+++ b/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/remote/Downloader.java
@@ -126,6 +126,9 @@
     Path knownPath = knownPaths.get(coordsToUse);
 
     if (knownPath != null && Files.exists(knownPath)) {
+      if (isPomPathForNonPomCoordinates(coordsToUse, knownPath)) {
+        return new DownloadResult(coordsToUse, Set.of(), null, null);
+      }
       pathInRepo = knownPath;
     } else {
       // Check the local cache for the path first
@@ -221,6 +224,10 @@
     return !JAR_PACKAGINGS.contains(extension);
   }
 
+  private boolean isPomPathForNonPomCoordinates(Coordinates coords, Path path) {
+    return path.getFileName().toString().endsWith(".pom") && !"pom".equals(coords.getExtension());
+  }
+
   private String calculateSha256(Path path) {
     try {
       byte[] bytes = Files.readAllBytes(path);
diff --git a/private/tools/prebuilt/lock_file_converter_deploy.jar b/private/tools/prebuilt/lock_file_converter_deploy.jar
index 0697a45..ef44459 100755
--- a/private/tools/prebuilt/lock_file_converter_deploy.jar
+++ b/private/tools/prebuilt/lock_file_converter_deploy.jar
Binary files differ
diff --git a/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/BUILD b/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/BUILD
index ba998bf..9ce5360 100644
--- a/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/BUILD
+++ b/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/BUILD
@@ -37,7 +37,10 @@
         "//private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/events",
         "//private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/gradle",
         "//private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/gradle/models",
+        "//private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/lockfile",
         "//private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/netrc",
+        "//private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/remote",
+        "//private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/ui",
         "//tests/com/github/bazelbuild/rules_jvm_external/resolver",
         artifact(
             "junit:junit",
diff --git a/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/GradleResolverTest.java b/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/GradleResolverTest.java
index a5cef57..b8a2c6b 100644
--- a/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/GradleResolverTest.java
+++ b/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/GradleResolverTest.java
@@ -16,27 +16,37 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 import com.github.bazelbuild.rules_jvm_external.Coordinates;
+import com.github.bazelbuild.rules_jvm_external.resolver.DependencyInfo;
 import com.github.bazelbuild.rules_jvm_external.resolver.MavenRepo;
 import com.github.bazelbuild.rules_jvm_external.resolver.ResolutionResult;
 import com.github.bazelbuild.rules_jvm_external.resolver.ResolvedArtifact;
 import com.github.bazelbuild.rules_jvm_external.resolver.Resolver;
 import com.github.bazelbuild.rules_jvm_external.resolver.ResolverTestBase;
+import com.github.bazelbuild.rules_jvm_external.resolver.cmd.AbstractMain;
 import com.github.bazelbuild.rules_jvm_external.resolver.cmd.ResolverConfig;
 import com.github.bazelbuild.rules_jvm_external.resolver.events.EventListener;
+import com.github.bazelbuild.rules_jvm_external.resolver.lockfile.V3LockFile;
 import com.github.bazelbuild.rules_jvm_external.resolver.netrc.Netrc;
+import com.github.bazelbuild.rules_jvm_external.resolver.remote.DownloadResult;
+import com.github.bazelbuild.rules_jvm_external.resolver.remote.Downloader;
+import com.github.bazelbuild.rules_jvm_external.resolver.ui.NullListener;
 import com.google.common.graph.Graph;
 import com.google.devtools.build.runfiles.AutoBazelRepository;
 import com.google.devtools.build.runfiles.Runfiles;
 import java.io.IOException;
+import java.net.URI;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
+import java.util.TreeMap;
 import java.util.stream.Collectors;
 import javax.xml.stream.XMLStreamException;
 import org.junit.Test;
@@ -254,6 +264,82 @@
   }
 
   @Test
+  public void lockFileHashIncludesNoBinaryBaseArtifactWithClassifiedArtifact() throws IOException {
+    Coordinates baseCoordinates = new Coordinates("com.example:native-lib:1.0");
+    Coordinates classifiedCoordinates =
+        new Coordinates("com.example:native-lib:jar:osx-aarch_64:1.0");
+    URI repo = URI.create("https://example.com/repo/");
+    Path classifiedArtifact = Files.createTempFile("native-lib", ".jar");
+    String classifiedSha = "sha-for-classified-artifact";
+
+    Map<String, Object> rendered =
+        new V3LockFile(
+                Set.of(repo),
+                Set.of(
+                    new DependencyInfo(
+                        baseCoordinates,
+                        Set.of(),
+                        Optional.empty(),
+                        Optional.empty(),
+                        Set.of(classifiedCoordinates),
+                        Set.of(),
+                        Set.of(),
+                        new TreeMap<>()),
+                    new DependencyInfo(
+                        classifiedCoordinates,
+                        Set.of(repo),
+                        Optional.of(classifiedArtifact),
+                        Optional.of(classifiedSha),
+                        Set.of(),
+                        Set.of(),
+                        Set.of(),
+                        new TreeMap<>())),
+                Set.of(),
+                false)
+            .render();
+
+    Map<String, Integer> hashes = AbstractMain.calculateArtifactHash(rendered);
+    assertTrue(hashes.containsKey(baseCoordinates.asKey()));
+    assertTrue(hashes.containsKey(classifiedCoordinates.asKey()));
+
+    @SuppressWarnings("unchecked")
+    Map<String, Map<String, Object>> artifacts =
+        (Map<String, Map<String, Object>>) rendered.get("artifacts");
+    @SuppressWarnings("unchecked")
+    Map<String, String> shasums =
+        (Map<String, String>) artifacts.get("com.example:native-lib").get("shasums");
+    assertTrue(shasums.containsKey("jar"));
+    assertNull(shasums.get("jar"));
+    assertEquals(classifiedSha, shasums.get("osx-aarch_64"));
+  }
+
+  @Test
+  public void downloaderDoesNotUsePomKnownPathAsBinaryForNonPomCoordinate() throws IOException {
+    Coordinates coordinates = new Coordinates("com.example:pom-backed:1.0");
+    MavenRepo mavenRepo = MavenRepo.create().add(coordinates);
+    Path pomPath =
+        mavenRepo
+            .getPath()
+            .resolve(coordinates.toRepoPath())
+            .getParent()
+            .resolve("pom-backed-1.0.pom");
+    Files.delete(mavenRepo.getPath().resolve(coordinates.toRepoPath()));
+
+    DownloadResult download =
+        new Downloader(
+                Netrc.fromUserHome(),
+                Files.createTempDirectory("local-repo"),
+                Set.of(mavenRepo.getPath().toUri()),
+                new NullListener(),
+                false,
+                Map.of(coordinates, pomPath))
+            .download(coordinates);
+
+    assertTrue(download.getPath().isEmpty());
+    assertTrue(download.getSha256().isEmpty());
+  }
+
+  @Test
   public void shouldRecordCorrectShaForResolvedVersionNotConflictingVersion() {
     // When there's a version conflict, the paths map should contain only the resolved version,
     // not the conflicting lower version. This ensures we record the correct SHA for the artifact.