View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.eclipse.aether.util.graph.transformer;
20  
21  import java.util.ArrayList;
22  import java.util.Collection;
23  import java.util.Collections;
24  import java.util.HashMap;
25  import java.util.IdentityHashMap;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.Objects;
29  
30  import org.eclipse.aether.ConfigurationProperties;
31  import org.eclipse.aether.RepositoryException;
32  import org.eclipse.aether.RepositorySystemSession;
33  import org.eclipse.aether.artifact.Artifact;
34  import org.eclipse.aether.collection.DependencyGraphTransformationContext;
35  import org.eclipse.aether.graph.DefaultDependencyNode;
36  import org.eclipse.aether.graph.Dependency;
37  import org.eclipse.aether.graph.DependencyNode;
38  import org.eclipse.aether.util.ConfigUtils;
39  import org.eclipse.aether.util.artifact.ArtifactIdUtils;
40  
41  import static java.util.Objects.requireNonNull;
42  
43  /**
44   * A dependency graph transformer that resolves version and scope conflicts among dependencies.
45   * This resolver builds a cycle-free parallel path tree from the dependency graph, then processes
46   * conflict groups in topologically sorted order to select winners.
47   * <p>
48   * For a given set of conflicting nodes, one node will be chosen as the winner. How losing nodes are handled
49   * depends on the configured verbosity level: they may be removed entirely, have their children removed, or
50   * be left in place with conflict information. The exact rules by which a winning node and its effective scope
51   * are determined are controlled by user-supplied implementations of {@link ConflictResolver.VersionSelector}, {@link ConflictResolver.ScopeSelector},
52   * {@link ConflictResolver.OptionalitySelector} and {@link ConflictResolver.ScopeDeriver}.
53   * <p>
54   * <strong>Algorithm Overview:</strong>
55   * <ol>
56   * <li><strong>Path Tree Construction:</strong> Builds a cycle-free parallel tree structure from the input
57   *     dependency graph, where each {@code Path} represents a unique route to a dependency node.
58   *     To avoid exponential memory use in highly connected graphs, subtree expansion of each
59   *     {@link DependencyNode} instance is bounded: when the same node is reached again via a
60   *     different parent path, a {@code Path} entry is still created for it (so all occurrences
61   *     appear in the conflict partition), but its subtree is only re-traversed if reached at a
62   *     strictly shallower depth than before.</li>
63   * <li><strong>Conflict Partitioning:</strong> Groups paths by conflict ID (based on groupId:artifactId:classifier:extension coordinates)</li>
64   * <li><strong>Topological Processing:</strong> Processes conflict groups in topologically sorted order</li>
65   * <li><strong>Winner Selection:</strong> Uses provided selectors to choose winners within each conflict group</li>
66   * <li><strong>Graph Transformation:</strong> Applies changes back to the original dependency graph</li>
67   * </ol>
68   * <p>
69   * <strong>Key Differences from {@link ClassicConflictResolver}:</strong>
70   * <ul>
71   * <li><strong>Memory Strategy:</strong> Uses parallel tree structure vs in-place graph modification</li>
72   * <li><strong>Cycle Handling:</strong> Explicitly breaks cycles during tree construction</li>
73   * <li><strong>Processing Order:</strong> Level-by-level from root vs depth-first traversal</li>
74   * </ul>
75   * <p>
76   * <strong>Implementation Note:</strong> This conflict resolver builds a cycle-free "parallel" structure based on the
77   * passed-in dependency graph, and applies operations level by level starting from the root. The parallel {@code Path}
78   * tree ensures that cycles in the original graph don't affect the conflict resolution algorithm's performance.
79   *
80   * @see ClassicConflictResolver
81   * @since 2.0.11
82   */
83  public final class PathConflictResolver extends ConflictResolver {
84      /**
85       * This implementation of conflict resolver is able to show more precise information regarding cycles in standard
86       * verbose mode. But, to make it really drop-in-replacement, we "tame down" this information. Still, users needing it
87       * may want to enable this for easier cycle detection, but in that case this conflict resolver will provide "extra nodes"
88       * not present on "standard verbosity level" with "classic" conflict resolver, that may lead to IT issues down the
89       * stream. Hence, the default is to provide as much information as much verbose "classic" does.
90       *
91       * @since 2.0.12
92       * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
93       * @configurationType {@link java.lang.Boolean}
94       * @configurationDefaultValue {@link #DEFAULT_SHOW_CYCLES_IN_STANDARD_VERBOSITY}
95       */
96      public static final String CONFIG_PROP_SHOW_CYCLES_IN_STANDARD_VERBOSITY = ConfigurationProperties.PREFIX_AETHER
97              + "conflictResolver." + ConflictResolver.PATH_CONFLICT_RESOLVER + ".showCyclesInStandardVerbosity";
98  
99      public static final boolean DEFAULT_SHOW_CYCLES_IN_STANDARD_VERBOSITY = false;
100 
101     private final ConflictResolver.VersionSelector versionSelector;
102     private final ConflictResolver.ScopeSelector scopeSelector;
103     private final ConflictResolver.ScopeDeriver scopeDeriver;
104     private final ConflictResolver.OptionalitySelector optionalitySelector;
105 
106     /**
107      * Creates a new conflict resolver instance with the specified hooks.
108      *
109      * @param versionSelector the version selector to use, must not be {@code null}
110      * @param scopeSelector the scope selector to use, must not be {@code null}
111      * @param optionalitySelector the optionality selector ot use, must not be {@code null}
112      * @param scopeDeriver the scope deriver to use, must not be {@code null}
113      */
114     public PathConflictResolver(
115             ConflictResolver.VersionSelector versionSelector,
116             ConflictResolver.ScopeSelector scopeSelector,
117             ConflictResolver.OptionalitySelector optionalitySelector,
118             ConflictResolver.ScopeDeriver scopeDeriver) {
119         this.versionSelector = requireNonNull(versionSelector, "version selector cannot be null");
120         this.scopeSelector = requireNonNull(scopeSelector, "scope selector cannot be null");
121         this.optionalitySelector = requireNonNull(optionalitySelector, "optionality selector cannot be null");
122         this.scopeDeriver = requireNonNull(scopeDeriver, "scope deriver cannot be null");
123     }
124 
125     @SuppressWarnings("unchecked")
126     @Override
127     public DependencyNode transformGraph(DependencyNode node, DependencyGraphTransformationContext context)
128             throws RepositoryException {
129         requireNonNull(node, "node cannot be null");
130         requireNonNull(context, "context cannot be null");
131         List<String> sortedConflictIds = (List<String>) context.get(TransformationContextKeys.SORTED_CONFLICT_IDS);
132         if (sortedConflictIds == null) {
133             ConflictIdSorter sorter = new ConflictIdSorter();
134             sorter.transformGraph(node, context);
135 
136             sortedConflictIds = (List<String>) context.get(TransformationContextKeys.SORTED_CONFLICT_IDS);
137         }
138 
139         @SuppressWarnings("unchecked")
140         Map<String, Object> stats = (Map<String, Object>) context.get(TransformationContextKeys.STATS);
141         long time1 = System.nanoTime();
142 
143         Map<DependencyNode, String> conflictIds =
144                 (Map<DependencyNode, String>) context.get(TransformationContextKeys.CONFLICT_IDS);
145         if (conflictIds == null) {
146             throw new RepositoryException("conflict groups have not been identified");
147         }
148 
149         State state = new State(
150                 ConflictResolver.getVerbosity(context.getSession()),
151                 ConfigUtils.getBoolean(
152                         context.getSession(),
153                         DEFAULT_SHOW_CYCLES_IN_STANDARD_VERBOSITY,
154                         CONFIG_PROP_SHOW_CYCLES_IN_STANDARD_VERBOSITY),
155                 versionSelector.getInstance(node, context),
156                 scopeSelector.getInstance(node, context),
157                 scopeDeriver.getInstance(node, context),
158                 optionalitySelector.getInstance(node, context),
159                 conflictIds,
160                 sortedConflictIds.size(),
161                 node);
162 
163         // loop over topographically sorted conflictIds
164         int conflictItemCount = 0;
165         for (String conflictId : sortedConflictIds) {
166             // paths in given conflict group to consider; filter out those moved out of scope
167             List<Path> allPaths = state.partitions.get(conflictId);
168             List<Path> activePaths = new ArrayList<>(allPaths.size());
169             List<ConflictItem> items = new ArrayList<>(allPaths.size());
170             for (Path p : allPaths) {
171                 if (!p.outOfScope) {
172                     activePaths.add(p);
173                     items.add(new ConflictItem(p));
174                 }
175             }
176             // Replace partition entry with filtered list to release references to out-of-scope
177             // paths (and their detached subtrees), allowing GC during resolution
178             state.partitions.put(conflictId, activePaths);
179             if (activePaths.isEmpty()) {
180                 // this means that whole group "fall out of scope" (are all on loser branches); skip
181                 continue;
182             }
183             conflictItemCount += activePaths.size();
184 
185             // create conflict context for given conflictId
186             ConflictContext ctx = new ConflictContext(node, state.conflictIds, items, conflictId);
187 
188             // select winner (is done by VersionSelector)
189             state.versionSelector.selectVersion(ctx);
190             if (ctx.winner == null) {
191                 throw new RepositoryException("conflict resolver did not select winner among " + items);
192             }
193             // select scope (no side effect between this and above operations)
194             state.scopeSelector.selectScope(ctx);
195             // select optionality (no side effect between this and above operations)
196             state.optionalitySelector.selectOptionality(ctx);
197 
198             // we have a winner path
199             Path winnerPath = ctx.winner.path;
200 
201             // mark conflictId as resolved with winner; sanity check
202             if (state.resolvedIds.containsKey(conflictId)) {
203                 throw new RepositoryException("conflict resolver already have winner for conflictId=" + conflictId
204                         + ": " + state.resolvedIds);
205             }
206             state.resolvedIds.put(conflictId, winnerPath);
207 
208             // loop over considered paths and apply selection results
209             for (Path path : activePaths) {
210                 // apply selected properties scope/optional to winner (winner carries version; others are losers)
211                 if (path == winnerPath) {
212                     path.scope = ctx.scope;
213                     path.optional = ctx.optional;
214                 }
215 
216                 // reset children as inheritance may be affected by this node scope/optionality change
217                 if (path.children != null) {
218                     for (Path c : path.children) {
219                         c.pull(0);
220                     }
221                 }
222                 // derive with new values from this to children only; observe winner flag
223                 path.derive(1, path == winnerPath);
224                 // push this node full level changes to DN graph
225                 path.push(0);
226             }
227         }
228 
229         if (stats != null) {
230             long time2 = System.nanoTime();
231             stats.put("ConflictResolver.totalTime", time2 - time1);
232             stats.put("ConflictResolver.conflictItemCount", conflictItemCount);
233         }
234 
235         return node;
236     }
237 
238     /**
239      * State of conflict resolution processing, to make this component (held in session) re-entrant by multiple threads.
240      */
241     private static class State {
242         /**
243          * Verbosity to be applied, see {@link ConflictResolver.Verbosity}.
244          */
245         private final ConflictResolver.Verbosity verbosity;
246 
247         /**
248          * Whether to show nodes entering cycles, for easier identification. If this is enabled, this implementation
249          * of conflict resolver will show more data than classic.
250          */
251         private final boolean showCyclesInStandardVerbosity;
252 
253         /**
254          * The {@link ConflictResolver.VersionSelector} to use.
255          */
256         private final ConflictResolver.VersionSelector versionSelector;
257 
258         /**
259          * The {@link ConflictResolver.ScopeSelector} to use.
260          */
261         private final ConflictResolver.ScopeSelector scopeSelector;
262 
263         /**
264          * The {@link ConflictResolver.ScopeDeriver} to use.
265          */
266         private final ConflictResolver.ScopeDeriver scopeDeriver;
267 
268         /**
269          * The {@link ConflictResolver.OptionalitySelector} to use/
270          */
271         private final ConflictResolver.OptionalitySelector optionalitySelector;
272 
273         /**
274          * The node to conflictId mapping from {@link ConflictMarker}.
275          */
276         private final Map<DependencyNode, String> conflictIds;
277 
278         /**
279          * A mapping from conflictId to paths represented as {@link Path}s that exist for each conflictId. In other
280          * words all paths to each {@link DependencyNode} that are member of same conflictId group.
281          * Uses {@link ArrayList} per partition; out-of-scope paths are marked via {@link Path#outOfScope} flag
282          * and filtered at query time, avoiding the per-entry overhead of LinkedHashSet/HashMap.Node.
283          */
284         private final Map<String, List<Path>> partitions;
285 
286         /**
287          * A mapping from conflictIds to winner {@link Path}, hence {@link DependencyNode}  for given conflictId.
288          */
289         private final Map<String, Path> resolvedIds;
290 
291         /**
292          * The root {@link Path}.
293          */
294         private final Path root;
295 
296         /**
297          * Pooled {@link ScopeContext} instance reused across derive() calls to avoid allocating a new
298          * object per node. Reset via {@link ScopeContext#reset(String, String)} before each use.
299          */
300         private final ScopeContext scopeContext;
301 
302         /**
303          * Tracks the minimum depth at which each {@link DependencyNode} instance has been expanded
304          * (i.e. its children visited) during {@link #gatherCRNodes(Path)}. Used to bound subtree
305          * re-traversal in highly connected graphs.
306          * <p>
307          * When the same {@link DependencyNode} is reached again via a different parent path, a
308          * {@link Path} entry is always created for it (so all occurrences appear in the conflict
309          * partition for winner selection). The subtree is only re-expanded if the new occurrence
310          * is at a strictly shallower depth than the recorded minimum — ensuring that the shallowest
311          * reachable occurrence drives expansion, while deeper duplicates are skipped.
312          * <p>
313          * Without this guard, a highly connected graph where node X is reachable via N different
314          * parents causes X's subtree to be expanded N times, leading to exponential {@link Path}
315          * creation and {@link OutOfMemoryError} on large multi-module reactors.
316          * <p>
317          * Uses {@link IdentityHashMap} because {@link DependencyNode} instances are shared objects
318          * in the dependency graph — identity equality is both correct and faster than equals/hashCode.
319          */
320         private final Map<DependencyNode, Integer> expandedNodes;
321 
322         @SuppressWarnings("checkstyle:ParameterNumber")
323         private State(
324                 ConflictResolver.Verbosity verbosity,
325                 boolean showCyclesInStandardVerbosity,
326                 ConflictResolver.VersionSelector versionSelector,
327                 ConflictResolver.ScopeSelector scopeSelector,
328                 ConflictResolver.ScopeDeriver scopeDeriver,
329                 ConflictResolver.OptionalitySelector optionalitySelector,
330                 Map<DependencyNode, String> conflictIds,
331                 int conflictIdCount,
332                 DependencyNode node)
333                 throws RepositoryException {
334             this.verbosity = verbosity;
335             this.showCyclesInStandardVerbosity = showCyclesInStandardVerbosity;
336             this.versionSelector = versionSelector;
337             this.scopeSelector = scopeSelector;
338             this.scopeDeriver = scopeDeriver;
339             this.optionalitySelector = optionalitySelector;
340             this.conflictIds = conflictIds;
341             // Right-size maps: conflictIdCount gives exact number of partitions and resolved entries
342             this.partitions = new HashMap<>(conflictIdCount * 4 / 3 + 1);
343             this.resolvedIds = new HashMap<>(conflictIdCount * 4 / 3 + 1);
344             this.scopeContext = new ScopeContext(null, null);
345             this.expandedNodes = new IdentityHashMap<>();
346             this.root = build(node);
347         }
348 
349         /**
350          * Consumes the dirty graph and builds internal structures out of {@link Path} instances that is always a
351          * tree. As a side effect, {@link #partitions} are being filled up as well, that combined with topo
352          * sorted conflictIds can serve as a starting to point to walk the graph.
353          */
354         private Path build(DependencyNode node) throws RepositoryException {
355             String nodeConflictId = this.conflictIds.get(node);
356             Path root = new Path(this, node, nodeConflictId, null);
357             gatherCRNodes(root);
358             return root;
359         }
360 
361         /**
362          * Iteratively builds {@link Path} graph by observing each node associated {@link DependencyNode}.
363          * Uses an explicit stack instead of recursion to avoid {@link StackOverflowError} on very deep
364          * dependency graphs (reported in large multi-module projects with 13+ levels of recursion).
365          * <p>
366          * Subtree expansion of each {@link DependencyNode} instance is bounded: when the same node is
367          * reached again via a different parent path, a {@link Path} entry is still created for it (so
368          * all occurrences appear in the conflict partition for winner selection), but its subtree is only
369          * re-traversed if reached at a strictly shallower depth than before. This prevents exponential
370          * {@link Path} creation in highly connected graphs (e.g. a 813-module reactor where each module
371          * depends on ~9 others).
372          */
373         private void gatherCRNodes(Path root) throws RepositoryException {
374             ArrayList<Path> stack = new ArrayList<>();
375             stack.add(root);
376             while (!stack.isEmpty()) {
377                 Path node = stack.remove(stack.size() - 1);
378                 List<DependencyNode> children = node.dn.getChildren();
379                 if (!children.isEmpty()) {
380                     // add children; we will get back those really added (not causing cycles)
381                     List<Path> added = node.addChildren(children);
382                     // push in reverse order so first child is processed first (DFS order),
383                     // but only if this DependencyNode instance hasn't been expanded at a shallower depth
384                     for (int i = added.size() - 1; i >= 0; i--) {
385                         Path child = added.get(i);
386                         Integer prevDepth = expandedNodes.get(child.dn);
387                         if (prevDepth == null || child.depth < prevDepth) {
388                             expandedNodes.put(child.dn, child.depth);
389                             stack.add(child);
390                         }
391                         // else: child.dn was already expanded at an equal or shallower depth;
392                         // the Path is already in the partition (created by addChildren), but we
393                         // skip re-expanding its subtree since the existing expansion already covered
394                         // all reachable descendants.
395                     }
396                 }
397             }
398         }
399     }
400 
401     /**
402      * Represents a unique path within the dependency graph from the root to a specific {@link DependencyNode}.
403      * This is the core data structure that enables the O(N) performance of {@link PathConflictResolver}.
404      * <p>
405      * <strong>Key Concepts:</strong>
406      * <ul>
407      * <li><strong>Path Uniqueness:</strong> Each {@code Path} instance represents a distinct route through
408      *     the dependency graph, even if multiple paths lead to the same {@code DependencyNode}</li>
409      * <li><strong>Cycle-Free Structure:</strong> The {@code Path} tree is guaranteed to be acyclic, even
410      *     when the original dependency graph contains cycles</li>
411      * <li><strong>Parallel Structure:</strong> This creates a "clean" tree alongside the original "dirty"
412      *     graph for efficient processing</li>
413      * </ul>
414      * <p>
415      * <strong>Example:</strong> If dependency A appears in the graph via two different routes:
416      * <pre>
417      * Root → B → A (path 1)
418      * Root → C → A (path 2)
419      * </pre>
420      * Two separate {@code Path} instances will be created, both pointing to the same {@code DependencyNode} A,
421      * but representing different paths through the dependency tree.
422      * <p>
423      * <strong>Memory Optimization:</strong> While this creates additional objects, it enables the algorithm
424      * to process conflicts in O(N) time rather than O(N²), making it much more efficient for large graphs.
425      * <p>
426      * <strong>Conflict Resolution:</strong> Paths are grouped by conflict ID (based on groupId:artifactId:classifier:extension coordinates),
427      * and the conflict resolution algorithm can efficiently process each group independently.
428      */
429     private static class Path {
430         // given
431         private final State state;
432         private DependencyNode dn;
433         private final String conflictId;
434         private final Path parent;
435         // derived
436         private final int depth;
437         // Lazy: null for leaf nodes (never populated by addChildren), right-sized for non-leaves.
438         // This avoids allocating an ArrayList + backing array for every leaf node in the tree
439         // (typically 60-70% of all nodes), saving ~40 bytes per leaf.
440         private List<Path> children;
441         // mutated
442         private String scope;
443         private boolean optional;
444         // Flag used instead of removing from partition sets; avoids LinkedHashSet overhead (~48 bytes/entry)
445         private boolean outOfScope;
446 
447         private Path(State state, DependencyNode dn, String conflictId, Path parent) {
448             this.state = state;
449             this.dn = dn;
450             this.conflictId = conflictId;
451             this.parent = parent;
452             this.depth = parent != null ? parent.depth + 1 : 0;
453             pull(0);
454 
455             this.state
456                     .partitions
457                     .computeIfAbsent(this.conflictId, k -> new ArrayList<>())
458                     .add(this);
459         }
460 
461         /**
462          * Checks whether the given conflictId appears on the path from this node to the root.
463          * Walks the parent chain comparing conflict IDs. Since dependency tree depth is bounded
464          * in practice (&lt; 30), each check is fast while avoiding per-node {@link java.util.HashSet}
465          * allocation that was a major JFR hotspot (~45% CPU) in large multi-module builds.
466          */
467         private boolean hasConflictIdOnPathToRoot(String targetConflictId) {
468             for (Path current = this; current != null; current = current.parent) {
469                 if (targetConflictId.equals(current.conflictId)) {
470                     return true;
471                 }
472             }
473             return false;
474         }
475 
476         /**
477          * Pulls (possibly updated) scope and optional values from associated {@link DependencyNode} to this instance,
478          * going down toward children recursively the required count of levels.
479          */
480         private void pull(int levels) {
481             Dependency d = dn.getDependency();
482             if (d != null) {
483                 this.scope = d.getScope();
484                 this.optional = d.isOptional();
485             } else {
486                 this.scope = "";
487                 this.optional = false;
488             }
489             int newLevels = levels - 1;
490             if (newLevels >= 0 && this.children != null) {
491                 for (Path child : this.children) {
492                     child.pull(newLevels);
493                 }
494             }
495         }
496 
497         /**
498          * Derives (from this to children direction) values that are "inherited" in tree: scope and optionality in the tree
499          * recursively going down required count of "levels".
500          */
501         private void derive(int levels, boolean winner) throws RepositoryException {
502             if (!winner) {
503                 if (this.parent != null) {
504                     if ((dn.getManagedBits() & DependencyNode.MANAGED_SCOPE) == 0) {
505                         state.scopeContext.reset(this.parent.scope, this.scope);
506                         state.scopeDeriver.deriveScope(state.scopeContext);
507                         this.scope = state.scopeContext.derivedScope;
508                     }
509                     if ((dn.getManagedBits() & DependencyNode.MANAGED_OPTIONAL) == 0) {
510                         if (!this.optional && this.parent.optional) {
511                             this.optional = true;
512                         }
513                     }
514                 } else {
515                     this.scope = "";
516                     this.optional = false;
517                 }
518             }
519             int newLevels = levels - 1;
520             if (newLevels >= 0 && this.children != null) {
521                 for (Path child : this.children) {
522                     child.derive(newLevels, false);
523                 }
524             }
525         }
526 
527         /**
528          * Pushes (applies) the scope and optional and structural changes to associated {@link DependencyNode} modifying
529          * the graph of it. Verbosity is observed, and depending on it the conflicting/loser nodes are removed, or
530          * just their children is removed (with special care for version ranges, see {@link #relatedSiblingsCount(Artifact, Path)}
531          * or by just doing nothing with them only marking losers in full verbosity mode.
532          */
533         private void push(int levels) {
534             if (this.parent != null) {
535                 Path winner = this.state.resolvedIds.get(this.conflictId);
536                 if (winner == null) {
537                     throw new IllegalStateException(
538                             "Winner selection did not happen for conflictId=" + this.conflictId);
539                 }
540                 if (!Objects.equals(winner.conflictId, this.conflictId)) {
541                     throw new IllegalStateException(
542                             "ConflictId mix-up: this=" + this.conflictId + " winner=" + winner.conflictId);
543                 }
544 
545                 if (winner == this) {
546                     // copy onto dn; if applicable
547                     if (this.dn.getDependency() != null) {
548                         this.dn.setData(
549                                 ConflictResolver.NODE_DATA_ORIGINAL_SCOPE,
550                                 this.dn.getDependency().getScope());
551                         this.dn.setData(
552                                 ConflictResolver.NODE_DATA_ORIGINAL_OPTIONALITY,
553                                 this.dn.getDependency().getOptional());
554                         this.dn.setScope(this.scope);
555                         this.dn.setOptional(this.optional);
556                     }
557                 } else {
558                     // loser; move out of scope
559                     moveOutOfScope();
560                     boolean markLoser = false;
561                     switch (state.verbosity) {
562                         case NONE:
563                             // remove loser dn
564                             if (this.parent.children != null) {
565                                 this.parent.children.remove(this);
566                             }
567                             this.parent.dn.setChildren(new ArrayList<>(this.parent.dn.getChildren()));
568                             this.parent.dn.getChildren().remove(this.dn);
569                             this.children = null;
570                             break;
571                         case STANDARD:
572                             // is redundant if:
573                             // - is not same as winner, and has related siblings (version range)
574                             // - same instance of DN is direct dependency on path leading here
575                             boolean isRedundant =
576                                     (!ArtifactIdUtils.equalsId(this.dn.getArtifact(), winner.dn.getArtifact())
577                                             && relatedSiblingsCount(this.dn.getArtifact(), this.parent) > 1);
578                             if (!this.state.showCyclesInStandardVerbosity) {
579                                 isRedundant = isRedundant
580                                         || this.parent.isDirectDependencyOnPathToRoot(this.dn.getArtifact());
581                             }
582                             if (isRedundant) {
583                                 // is redundant dn; remove dn
584                                 if (this.parent.children != null) {
585                                     this.parent.children.remove(this);
586                                 }
587                                 this.parent.dn.setChildren(new ArrayList<>(this.parent.dn.getChildren()));
588                                 this.parent.dn.getChildren().remove(this.dn);
589                                 this.children = null;
590                             } else {
591                                 // copy loser dn; without children
592                                 DependencyNode dnCopy = new DefaultDependencyNode(this.dn);
593                                 dnCopy.setChildren(Collections.emptyList());
594 
595                                 // swap it out in DN graph; in case of cycles this may happen more than once
596                                 int idx = this.parent.dn.getChildren().indexOf(this.dn);
597                                 if (idx >= 0) {
598                                     this.parent.dn.getChildren().set(idx, dnCopy);
599                                 }
600                                 this.dn = dnCopy;
601 
602                                 this.children = null;
603                                 markLoser = true;
604                             }
605                             break;
606                         case FULL:
607                             // copy loser dn; with children
608                             DependencyNode dnCopy = new DefaultDependencyNode(this.dn);
609                             dnCopy.setChildren(new ArrayList<>(this.dn.getChildren()));
610 
611                             // swap it out in DN graph; in case of cycles this may happen more than once
612                             int idx = this.parent.dn.getChildren().indexOf(this.dn);
613                             if (idx >= 0) {
614                                 this.parent.dn.getChildren().set(idx, dnCopy);
615                             }
616                             this.dn = dnCopy;
617 
618                             markLoser = true;
619                             break;
620                         default:
621                             throw new IllegalArgumentException("Unknown " + state.verbosity);
622                     }
623                     if (markLoser) {
624                         this.dn.setData(ConflictResolver.NODE_DATA_WINNER, winner.dn);
625                         this.dn.setData(
626                                 ConflictResolver.NODE_DATA_ORIGINAL_SCOPE,
627                                 this.dn.getDependency().getScope());
628                         this.dn.setData(
629                                 ConflictResolver.NODE_DATA_ORIGINAL_OPTIONALITY,
630                                 this.dn.getDependency().getOptional());
631                         this.dn.setScope(this.scope);
632                         this.dn.setOptional(this.optional);
633                     }
634                 }
635             }
636 
637             // Note: push() is always called with levels=0, so newLevels would be -1
638             // and the recursive block would never execute. The recursive structure is
639             // intentionally not present; all push() calls happen from the main loop.
640         }
641 
642         /**
643          * Returns {@code true} if given artifact is a direct dependency on the path leading from this toward root.
644          * A "direct dependency" is one at depth 1 (immediate child of root). Rather than recursing through every
645          * ancestor, this walks directly to the depth-1 node and performs one allocation-free comparison.
646          * <p>
647          * Note: this check and use of this method is ONLY present to make this conflict resolver produce SAME output
648          * as {@link ClassicConflictResolver} does, but IMHO this rule here is very arbitrary, moreover, in "standard"
649          * (where it is only used) verbosity it in facts HIDES the trace of possible cycles.
650          *
651          * @see #CONFIG_PROP_SHOW_CYCLES_IN_STANDARD_VERBOSITY
652          */
653         private boolean isDirectDependencyOnPathToRoot(Artifact artifact) {
654             // Walk up to depth-1 ancestor (direct dependency of root) instead of recursing every level
655             Path current = this;
656             while (current != null && current.depth > 1) {
657                 current = current.parent;
658             }
659             return current != null
660                     && current.depth == 1
661                     && ArtifactIdUtils.equalsVersionlessId(current.dn.getArtifact(), artifact);
662         }
663 
664         /**
665          * Counts "relatives" (GACE equal) artifacts under same parent; this is for cleaning up redundant nodes in
666          * case of version ranges, where same GACE is resolved into multiple GACEV as range is resolved. In {@link ConflictResolver.Verbosity#STANDARD}
667          * verbosity mode we remove "redundant" nodes (of a range) leaving only "winner equal" loser, that have same GACEV as winner.
668          */
669         private int relatedSiblingsCount(Artifact artifact, Path parent) {
670             if (parent.children == null) {
671                 return 0;
672             }
673             String groupId = artifact.getGroupId();
674             String artifactId = artifact.getArtifactId();
675             int count = 0;
676             for (Path n : parent.children) {
677                 Artifact a = n.dn.getArtifact();
678                 if (Objects.equals(groupId, a.getGroupId()) && Objects.equals(artifactId, a.getArtifactId())) {
679                     count++;
680                 }
681             }
682             return count;
683         }
684 
685         /**
686          * Marks this and all child {@link Path} nodes as out of scope; essentially marks whole subtree
687          * from "this and below" as loser, to not be considered in subsequent winner selections.
688          * Uses a boolean flag instead of removing from partition collections, avoiding the per-entry
689          * overhead of LinkedHashSet (~48 bytes/entry). Out-of-scope paths are filtered at query time.
690          * <p>
691          * Uses an explicit stack instead of recursion to avoid {@link StackOverflowError} on deep
692          * dependency graphs, consistent with the iterative approach in
693          * {@link State#gatherCRNodes(Path)}. Also nulls children references on out-of-scope nodes
694          * to allow GC of detached subtrees during resolution.
695          */
696         private void moveOutOfScope() {
697             ArrayList<Path> stack = new ArrayList<>();
698             stack.add(this);
699             while (!stack.isEmpty()) {
700                 Path node = stack.remove(stack.size() - 1);
701                 node.outOfScope = true;
702                 if (node.children != null) {
703                     stack.addAll(node.children);
704                     node.children = null;
705                 }
706             }
707         }
708 
709         /**
710          * Adds node children: this method should be "batch" used, as all (potential) children should be added at once.
711          * Method will return really added {@link Path} instances, as this class avoids cycles. Those forming a cycle
712          * are not recursed (not returned in list), keeping {@link Path} cycle free.
713          * <p>
714          * Cycle detection is performed via {@link #hasConflictIdOnPathToRoot(String)} which walks
715          * the parent chain comparing conflict IDs. Since dependency tree depth is bounded in
716          * practice (&lt; 30), each check is fast while avoiding per-node HashSet allocation.
717          * This implies that this conflict resolver, by its nature "redoes" the
718          * {@link TransformationContextKeys#CYCLIC_CONFLICT_IDS} calculated by {@link ConflictIdSorter}.
719          */
720         private List<Path> addChildren(List<DependencyNode> children) throws RepositoryException {
721             // Right-size the children list to avoid ArrayList default capacity waste
722             this.children = new ArrayList<>(children.size());
723             ArrayList<Path> added = new ArrayList<>(children.size());
724             for (DependencyNode child : children) {
725                 String childConflictId = this.state.conflictIds.get(child);
726                 boolean cycle = hasConflictIdOnPathToRoot(childConflictId);
727                 Path c = new Path(this.state, child, childConflictId, this);
728                 this.children.add(c);
729                 c.derive(0, false);
730                 if (!cycle) {
731                     added.add(c);
732                 }
733             }
734             return added;
735         }
736 
737         /**
738          * Dump for debug.
739          */
740         private void dump(String padding) {
741             System.out.println(padding + this.dn + ": " + this.scope + "/" + this.optional);
742             if (this.children != null) {
743                 for (Path child : this.children) {
744                     child.dump(padding + "  ");
745                 }
746             }
747         }
748 
749         /**
750          * For easier debug.
751          */
752         @Override
753         public String toString() {
754             return this.dn.toString();
755         }
756     }
757 
758     /**
759      * A context used to hold information that is relevant for deriving the scope of a child dependency.
760      *
761      * @see ConflictResolver.ScopeDeriver
762      * @noinstantiate This class is not intended to be instantiated by clients in production code, the constructor may
763      *                change without notice and only exists to enable unit testing
764      */
765     private static final class ScopeContext extends ConflictResolver.ScopeContext {
766         private String parentScope;
767         private String childScope;
768         private String derivedScope;
769 
770         /**
771          * Creates a new scope context with the specified properties.
772          *
773          * @param parentScope the scope of the parent dependency, may be {@code null}
774          * @param childScope the scope of the child dependency, may be {@code null}
775          * @noreference This class is not intended to be instantiated by clients in production code, the constructor may
776          *              change without notice and only exists to enable unit testing
777          */
778         private ScopeContext(String parentScope, String childScope) {
779             this.parentScope = (parentScope != null) ? parentScope : "";
780             this.derivedScope = (childScope != null) ? childScope : "";
781             this.childScope = (childScope != null) ? childScope : "";
782         }
783 
784         /**
785          * Resets this context for reuse, avoiding allocation of a new instance per derive() call.
786          */
787         private void reset(String parentScope, String childScope) {
788             this.parentScope = (parentScope != null) ? parentScope : "";
789             this.derivedScope = (childScope != null) ? childScope : "";
790             this.childScope = (childScope != null) ? childScope : "";
791         }
792 
793         /**
794          * Gets the scope of the parent dependency. This is usually the scope that was derived by earlier invocations of
795          * the scope deriver.
796          *
797          * @return the scope of the parent dependency, never {@code null}
798          */
799         public String getParentScope() {
800             return parentScope;
801         }
802 
803         /**
804          * Gets the original scope of the child dependency. This is the scope that was declared in the artifact
805          * descriptor of the parent dependency.
806          *
807          * @return the original scope of the child dependency, never {@code null}
808          */
809         public String getChildScope() {
810             return childScope;
811         }
812 
813         /**
814          * Gets the derived scope of the child dependency. This is initially equal to {@link #getChildScope()} until the
815          * scope deriver makes changes.
816          *
817          * @return the derived scope of the child dependency, never {@code null}
818          */
819         public String getDerivedScope() {
820             return derivedScope;
821         }
822 
823         /**
824          * Sets the derived scope of the child dependency.
825          *
826          * @param derivedScope the derived scope of the dependency, may be {@code null}
827          */
828         public void setDerivedScope(String derivedScope) {
829             this.derivedScope = (derivedScope != null) ? derivedScope : "";
830         }
831     }
832 
833     /**
834      * A conflicting dependency.
835      *
836      * @noinstantiate This class is not intended to be instantiated by clients in production code, the constructor may
837      *                change without notice and only exists to enable unit testing
838      */
839     private static final class ConflictItem extends ConflictResolver.ConflictItem {
840         private final Path path;
841         private final List<DependencyNode> parent;
842         private final Artifact artifact;
843         private final DependencyNode node;
844         private final int depth;
845         private final String scope;
846         private final int optionalities;
847 
848         private ConflictItem(Path path) {
849             this.path = path;
850             if (path.parent != null) {
851                 DependencyNode parent = path.parent.dn;
852                 this.parent = parent.getChildren();
853                 this.artifact = parent.getArtifact();
854             } else {
855                 this.parent = null;
856                 this.artifact = null;
857             }
858             this.node = path.dn;
859             this.depth = path.depth;
860             this.scope = path.scope;
861             this.optionalities = path.optional ? OPTIONAL_TRUE : OPTIONAL_FALSE;
862         }
863 
864         /**
865          * Determines whether the specified conflict item is a sibling of this item.
866          *
867          * @param item the other conflict item, must not be {@code null}
868          * @return {@code true} if the given item has the same parent as this item, {@code false} otherwise
869          */
870         @Override
871         public boolean isSibling(ConflictResolver.ConflictItem item) {
872             return parent == ((ConflictItem) item).parent;
873         }
874 
875         /**
876          * Gets the dependency node involved in the conflict.
877          *
878          * @return the involved dependency node, never {@code null}
879          */
880         @Override
881         public DependencyNode getNode() {
882             return node;
883         }
884 
885         /**
886          * Gets the dependency involved in the conflict, short for {@code getNode.getDependency()}.
887          *
888          * @return the involved dependency, never {@code null}
889          */
890         @Override
891         public Dependency getDependency() {
892             return node.getDependency();
893         }
894 
895         /**
896          * Gets the zero-based depth at which the conflicting node occurs in the graph. As such, the depth denotes the
897          * number of parent nodes. If actually multiple paths lead to the node, the return value denotes the smallest
898          * possible depth.
899          *
900          * @return the zero-based depth of the node in the graph
901          */
902         @Override
903         public int getDepth() {
904             return depth;
905         }
906 
907         /**
908          * Gets the derived scopes of the dependency. In general, the same dependency node could be reached via
909          * different paths and each path might result in a different derived scope.
910          *
911          * @return the (read-only) set of derived scopes of the dependency, never {@code null}
912          * @see ConflictResolver.ScopeDeriver
913          */
914         @Override
915         public Collection<String> getScopes() {
916             return Collections.singleton(scope);
917         }
918 
919         /**
920          * Gets the derived optionalities of the dependency. In general, the same dependency node could be reached via
921          * different paths and each path might result in a different derived optionality.
922          *
923          * @return a bit field consisting of {@link PathConflictResolver.ConflictItem#OPTIONAL_FALSE} and/or
924          *         {@link PathConflictResolver.ConflictItem#OPTIONAL_TRUE} indicating the derived optionalities the
925          *         dependency was encountered with
926          */
927         @Override
928         public int getOptionalities() {
929             return optionalities;
930         }
931 
932         @Override
933         public String toString() {
934             return node + " @ " + depth + " < " + artifact;
935         }
936     }
937 
938     /**
939      * A context used to hold information that is relevant for resolving version and scope conflicts.
940      *
941      * @see ConflictResolver.VersionSelector
942      * @see ConflictResolver.ScopeSelector
943      * @noinstantiate This class is not intended to be instantiated by clients in production code, the constructor may
944      *                change without notice and only exists to enable unit testing
945      */
946     private static final class ConflictContext extends ConflictResolver.ConflictContext {
947         private final DependencyNode root;
948         private final Map<DependencyNode, String> conflictIds;
949         private final Collection<ConflictResolver.ConflictItem> items;
950         private final String conflictId;
951 
952         // elected properties
953         private ConflictItem winner;
954         private String scope;
955         private Boolean optional;
956 
957         private ConflictContext(
958                 DependencyNode root,
959                 Map<DependencyNode, String> conflictIds,
960                 Collection<ConflictItem> items,
961                 String conflictId) {
962             this.root = root;
963             this.conflictIds = conflictIds;
964             this.items = Collections.unmodifiableCollection(items);
965             this.conflictId = conflictId;
966         }
967 
968         /**
969          * Gets the root node of the dependency graph being transformed.
970          *
971          * @return the root node of the dependency graph, never {@code null}
972          */
973         @Override
974         public DependencyNode getRoot() {
975             return root;
976         }
977 
978         /**
979          * Determines whether the specified dependency node belongs to this conflict context.
980          *
981          * @param node the dependency node to check, must not be {@code null}
982          * @return {@code true} if the given node belongs to this conflict context, {@code false} otherwise
983          */
984         @Override
985         public boolean isIncluded(DependencyNode node) {
986             return conflictId.equals(conflictIds.get(node));
987         }
988 
989         /**
990          * Gets the collection of conflict items in this context.
991          *
992          * @return the (read-only) collection of conflict items in this context, never {@code null}
993          */
994         @Override
995         public Collection<ConflictResolver.ConflictItem> getItems() {
996             return items;
997         }
998 
999         /**
1000          * Gets the conflict item which has been selected as the winner among the conflicting dependencies.
1001          *
1002          * @return the winning conflict item or {@code null} if not set yet
1003          */
1004         @Override
1005         public ConflictResolver.ConflictItem getWinner() {
1006             return winner;
1007         }
1008 
1009         /**
1010          * Sets the conflict item which has been selected as the winner among the conflicting dependencies.
1011          *
1012          * @param winner the winning conflict item, may be {@code null}
1013          */
1014         @Override
1015         public void setWinner(ConflictResolver.ConflictItem winner) {
1016             this.winner = (ConflictItem) winner;
1017         }
1018 
1019         /**
1020          * Gets the effective scope of the winning dependency.
1021          *
1022          * @return the effective scope of the winning dependency or {@code null} if none
1023          */
1024         @Override
1025         public String getScope() {
1026             return scope;
1027         }
1028 
1029         /**
1030          * Sets the effective scope of the winning dependency.
1031          *
1032          * @param scope the effective scope, may be {@code null}
1033          */
1034         @Override
1035         public void setScope(String scope) {
1036             this.scope = scope;
1037         }
1038 
1039         /**
1040          * Gets the effective optional flag of the winning dependency.
1041          *
1042          * @return the effective optional flag or {@code null} if none
1043          */
1044         @Override
1045         public Boolean getOptional() {
1046             return optional;
1047         }
1048 
1049         /**
1050          * Sets the effective optional flag of the winning dependency.
1051          *
1052          * @param optional the effective optional flag, may be {@code null}
1053          */
1054         @Override
1055         public void setOptional(Boolean optional) {
1056             this.optional = optional;
1057         }
1058 
1059         @Override
1060         public String toString() {
1061             return winner + " @ " + scope + " < " + items;
1062         }
1063     }
1064 }