001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.eclipse.aether.util.graph.transformer;
020
021import java.util.ArrayList;
022import java.util.Collection;
023import java.util.Collections;
024import java.util.HashMap;
025import java.util.IdentityHashMap;
026import java.util.List;
027import java.util.Map;
028import java.util.Objects;
029
030import org.eclipse.aether.ConfigurationProperties;
031import org.eclipse.aether.RepositoryException;
032import org.eclipse.aether.RepositorySystemSession;
033import org.eclipse.aether.artifact.Artifact;
034import org.eclipse.aether.collection.DependencyGraphTransformationContext;
035import org.eclipse.aether.graph.DefaultDependencyNode;
036import org.eclipse.aether.graph.Dependency;
037import org.eclipse.aether.graph.DependencyNode;
038import org.eclipse.aether.util.ConfigUtils;
039import org.eclipse.aether.util.artifact.ArtifactIdUtils;
040
041import static java.util.Objects.requireNonNull;
042
043/**
044 * A dependency graph transformer that resolves version and scope conflicts among dependencies.
045 * This resolver builds a cycle-free parallel path tree from the dependency graph, then processes
046 * conflict groups in topologically sorted order to select winners.
047 * <p>
048 * For a given set of conflicting nodes, one node will be chosen as the winner. How losing nodes are handled
049 * depends on the configured verbosity level: they may be removed entirely, have their children removed, or
050 * be left in place with conflict information. The exact rules by which a winning node and its effective scope
051 * are determined are controlled by user-supplied implementations of {@link ConflictResolver.VersionSelector}, {@link ConflictResolver.ScopeSelector},
052 * {@link ConflictResolver.OptionalitySelector} and {@link ConflictResolver.ScopeDeriver}.
053 * <p>
054 * <strong>Algorithm Overview:</strong>
055 * <ol>
056 * <li><strong>Path Tree Construction:</strong> Builds a cycle-free parallel tree structure from the input
057 *     dependency graph, where each {@code Path} represents a unique route to a dependency node.
058 *     To avoid exponential memory use in highly connected graphs, subtree expansion of each
059 *     {@link DependencyNode} instance is bounded: when the same node is reached again via a
060 *     different parent path, a {@code Path} entry is still created for it (so all occurrences
061 *     appear in the conflict partition), but its subtree is only re-traversed if reached at a
062 *     strictly shallower depth than before.</li>
063 * <li><strong>Conflict Partitioning:</strong> Groups paths by conflict ID (based on groupId:artifactId:classifier:extension coordinates)</li>
064 * <li><strong>Topological Processing:</strong> Processes conflict groups in topologically sorted order</li>
065 * <li><strong>Winner Selection:</strong> Uses provided selectors to choose winners within each conflict group</li>
066 * <li><strong>Graph Transformation:</strong> Applies changes back to the original dependency graph</li>
067 * </ol>
068 * <p>
069 * <strong>Key Differences from {@link ClassicConflictResolver}:</strong>
070 * <ul>
071 * <li><strong>Memory Strategy:</strong> Uses parallel tree structure vs in-place graph modification</li>
072 * <li><strong>Cycle Handling:</strong> Explicitly breaks cycles during tree construction</li>
073 * <li><strong>Processing Order:</strong> Level-by-level from root vs depth-first traversal</li>
074 * </ul>
075 * <p>
076 * <strong>Implementation Note:</strong> This conflict resolver builds a cycle-free "parallel" structure based on the
077 * passed-in dependency graph, and applies operations level by level starting from the root. The parallel {@code Path}
078 * tree ensures that cycles in the original graph don't affect the conflict resolution algorithm's performance.
079 *
080 * @see ClassicConflictResolver
081 * @since 2.0.11
082 */
083public final class PathConflictResolver extends ConflictResolver {
084    /**
085     * This implementation of conflict resolver is able to show more precise information regarding cycles in standard
086     * verbose mode. But, to make it really drop-in-replacement, we "tame down" this information. Still, users needing it
087     * may want to enable this for easier cycle detection, but in that case this conflict resolver will provide "extra nodes"
088     * not present on "standard verbosity level" with "classic" conflict resolver, that may lead to IT issues down the
089     * stream. Hence, the default is to provide as much information as much verbose "classic" does.
090     *
091     * @since 2.0.12
092     * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
093     * @configurationType {@link java.lang.Boolean}
094     * @configurationDefaultValue {@link #DEFAULT_SHOW_CYCLES_IN_STANDARD_VERBOSITY}
095     */
096    public static final String CONFIG_PROP_SHOW_CYCLES_IN_STANDARD_VERBOSITY = ConfigurationProperties.PREFIX_AETHER
097            + "conflictResolver." + ConflictResolver.PATH_CONFLICT_RESOLVER + ".showCyclesInStandardVerbosity";
098
099    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}