88import org .apache .maven .project .DependencyResolutionResult ;
99import org .apache .maven .project .MavenProject ;
1010import org .apache .maven .project .ProjectDependenciesResolver ;
11+ import org .eclipse .aether .RepositorySystem ;
1112import org .eclipse .aether .artifact .Artifact ;
1213import org .eclipse .aether .graph .Dependency ;
1314import org .eclipse .aether .graph .DependencyFilter ;
1415import org .eclipse .aether .graph .DependencyNode ;
1516import org .eclipse .aether .resolution .ArtifactDescriptorException ;
17+ import org .eclipse .aether .resolution .ArtifactResult ;
18+ import org .eclipse .aether .resolution .DependencyRequest ;
1619import org .eclipse .aether .resolution .VersionRangeResolutionException ;
1720import org .slf4j .Logger ;
1821
@@ -72,12 +75,15 @@ public static final class Options {
7275 private static final String NODE_DATA_CONFLICT_WINNER = "conflict.winner" ;
7376
7477 private final ProjectDependenciesResolver dependenciesResolver ;
78+ private final RepositorySystem repositorySystem ;
7579 private final String mavenVersion ;
7680 private final Logger log ;
7781
7882 public SocketFactsRecordsEngine (
79- ProjectDependenciesResolver dependenciesResolver , String mavenVersion , Logger log ) {
83+ ProjectDependenciesResolver dependenciesResolver , RepositorySystem repositorySystem , String mavenVersion ,
84+ Logger log ) {
8085 this .dependenciesResolver = dependenciesResolver ;
86+ this .repositorySystem = repositorySystem ;
8187 this .mavenVersion = mavenVersion ;
8288 this .log = log ;
8389 }
@@ -166,10 +172,11 @@ private void collectModule(
166172 String moduleCoord = module .getGroupId () + ":" + module .getArtifactId () + ":" + module .getVersion ();
167173 DependencyResolutionResult result ;
168174 DependencyResolutionException thrown = null ;
175+ DependencyFilter resolutionFilter = materializationFilter (opts , passingScopes , reactorGavs , populateGavs );
169176 try {
170177 DependencyResolutionRequest request =
171178 new DefaultDependencyResolutionRequest (module , session .getRepositorySession ());
172- request .setResolutionFilter (materializationFilter ( opts , passingScopes , reactorGavs , populateGavs ) );
179+ request .setResolutionFilter (resolutionFilter );
173180 result = dependenciesResolver .resolve (request );
174181 } catch (DependencyResolutionException e ) {
175182 // Maven attaches the partial result — graph plus per-dependency errors — to the exception, so
@@ -205,13 +212,16 @@ private void collectModule(
205212 log .warn ("[socket-facts] could not build dependency graph for " + moduleCoord + ": " + rootMessage (e ));
206213 reported = true ;
207214 }
215+ // Maven throws on a collection error before it fetches any artifact, which would leave every
216+ // healthy node of the partial graph without its file.
217+ Set <String > unfetched = new HashSet <>();
218+ if (!result .getCollectionErrors ().isEmpty () && graph != null && opts .withFiles ) {
219+ materialize (session , graph , resolutionFilter , unresolved , moduleCoord , failures , unfetched );
220+ }
208221 for (Dependency dep : result .getUnresolvedDependencies ()) {
209222 Artifact artifact = dep .getArtifact ();
210- if (artifact == null ) continue ;
211- List <Exception > errors = result .getResolutionErrors (dep );
212- failures .add (new Failure (
213- gav (artifact ), rootMessage (errors .isEmpty () ? null : errors .get (0 )), scopeOf (dep ), moduleCoord ));
214- log .debug ("[socket-facts] could not materialize " + artifact + " (" + scopeOf (dep ) + ")" );
223+ if (artifact == null || unresolved .contains (gav (artifact ))) continue ;
224+ reportUnfetched (dep , result .getResolutionErrors (dep ), moduleCoord , failures , unfetched );
215225 reported = true ;
216226 }
217227 // Fail closed: a throw whose result named nothing must still surface, or an unresolved dependency
@@ -233,6 +243,10 @@ private void collectModule(
233243 if (isProd (scopeOf (child .getDependency ()))) directProdIds .add (id );
234244 }
235245 }
246+ for (String id : unfetched ) {
247+ Node node = nodes .get (id );
248+ if (node != null ) node .unfetched = true ;
249+ }
236250 }
237251
238252 /**
@@ -275,6 +289,47 @@ public boolean accept(DependencyNode node, List<DependencyNode> parents) {
275289 };
276290 }
277291
292+ /**
293+ * The artifact step Maven skipped, run on the partial graph the way the resolver's own
294+ * {@code resolveDependencies} continues past a collection failure. The failed dependencies are left
295+ * out: they are never emitted, and fetching them again would only repeat the failure.
296+ */
297+ private void materialize (
298+ MavenSession session ,
299+ DependencyNode graph ,
300+ final DependencyFilter resolutionFilter ,
301+ final Set <String > unresolved ,
302+ String moduleCoord ,
303+ Set <Failure > failures ,
304+ Set <String > unfetched ) {
305+ DependencyFilter healthy = new DependencyFilter () {
306+ @ Override
307+ public boolean accept (DependencyNode node , List <DependencyNode > parents ) {
308+ return resolutionFilter .accept (node , parents ) && !unresolved .contains (gav (node .getArtifact ()));
309+ }
310+ };
311+ List <ArtifactResult > results ;
312+ try {
313+ results = repositorySystem
314+ .resolveDependencies (session .getRepositorySession (), new DependencyRequest (graph , healthy ))
315+ .getArtifactResults ();
316+ } catch (org .eclipse .aether .resolution .DependencyResolutionException e ) {
317+ results = e .getResult ().getArtifactResults ();
318+ }
319+ for (ArtifactResult ar : results ) {
320+ if (ar .isResolved ()) continue ;
321+ reportUnfetched (ar .getRequest ().getDependencyNode ().getDependency (), ar .getExceptions (), moduleCoord , failures , unfetched );
322+ }
323+ }
324+
325+ private void reportUnfetched (
326+ Dependency dep , List <Exception > errors , String moduleCoord , Set <Failure > failures , Set <String > unfetched ) {
327+ String id = externalId (dep .getArtifact ());
328+ failures .add (new Failure (id , rootMessage (errors .isEmpty () ? null : errors .get (0 )), scopeOf (dep ), moduleCoord ));
329+ unfetched .add (id );
330+ log .debug ("[socket-facts] could not materialize " + dep .getArtifact () + " (" + scopeOf (dep ) + ")" );
331+ }
332+
278333 private String visit (
279334 DependencyNode dn ,
280335 Set <String > passingScopes ,
@@ -295,16 +350,12 @@ private String visit(
295350 String gav = gav (artifact );
296351 if (unresolved .contains (gav )) return null ;
297352 boolean internal = reactorGavs .contains (gav );
298- // Maven's `type` rather than aether's file extension, so a test-jar keeps the coordId the
299- // assembler and the Gradle/SBT scripts already emit.
300353 String type = artifact .getProperty (ARTIFACT_PROPERTY_TYPE , artifact .getExtension ());
301354 String classifier = artifact .getClassifier ();
302- // Base version: a resolved remote snapshot's `version` is the timestamped build, which would
303- // put a coordinate in the records that no manifest ever names.
304355 String version = artifact .getBaseVersion ();
305356 String id = internal
306357 ? SocketSupport .bareId (artifact .getGroupId (), artifact .getArtifactId (), version )
307- : SocketSupport . coordId (artifact . getGroupId (), artifact . getArtifactId (), type , classifier , version );
358+ : externalId (artifact );
308359
309360 // One walk per node per module traversal: a shared subtree reached via another edge is already
310361 // fully recorded (node, children, resolved file), so hand back the id without re-descending.
@@ -329,6 +380,17 @@ private String visit(
329380 return id ;
330381 }
331382
383+ private static String externalId (Artifact artifact ) {
384+ // Maven's `type` rather than aether's file extension, so a test-jar keeps the coordId the
385+ // assembler and the Gradle/SBT scripts already emit. Base version: a resolved remote snapshot's
386+ // `version` is the timestamped build, which would put a coordinate in the records that no
387+ // manifest ever names.
388+ return SocketSupport .coordId (
389+ artifact .getGroupId (), artifact .getArtifactId (),
390+ artifact .getProperty (ARTIFACT_PROPERTY_TYPE , artifact .getExtension ()), artifact .getClassifier (),
391+ artifact .getBaseVersion ());
392+ }
393+
332394 private static String gav (Artifact artifact ) {
333395 return artifact .getGroupId () + ":" + artifact .getArtifactId () + ":" + artifact .getBaseVersion ();
334396 }
@@ -405,6 +467,7 @@ private int emitRoot(
405467 if (nodeMap .containsKey (child )) rec (lines , "edge" , rootId , n .id , child );
406468 }
407469 for (String f : n .files ) rec (lines , "file" , rootId , n .id , f );
470+ if (n .unfetched ) rec (lines , "unfetched" , rootId , n .id );
408471 }
409472 return rootIdx + 1 ;
410473 }
@@ -519,7 +582,11 @@ private static String rootMessage(Throwable t) {
519582 int guard = 0 ;
520583 while (cur != null && guard ++ < 12 ) {
521584 if (cur .getMessage () != null ) msg = cur .getMessage ();
522- cur = cur .getCause ();
585+ Throwable next = cur .getCause ();
586+ // Maven 4 batches requests: BatchRequestException carries each failed request as a suppressed
587+ // exception, not as its cause.
588+ if (next == null && cur .getSuppressed ().length > 0 ) next = cur .getSuppressed ()[0 ];
589+ cur = next ;
523590 }
524591 return (msg != null ? msg : "unknown resolution failure" ).trim ();
525592 }
@@ -623,6 +690,7 @@ private static final class Node {
623690 final String project ;
624691 final TreeSet <String > children = new TreeSet <>();
625692 final TreeSet <String > files = new TreeSet <>();
693+ boolean unfetched ;
626694
627695 Node (String id , String groupId , String artifactId , String type , String classifier , String version , String project ) {
628696 this .id = id ;
0 commit comments