File: RestoreCommand\LockFileBuilder.cs
Web Access
Project: src\src\nuget-client\src\NuGet.Core\NuGet.Commands\NuGet.Commands.csproj (NuGet.Commands)
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

#nullable disable

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using NuGet.Common;
using NuGet.DependencyResolver;
using NuGet.Frameworks;
using NuGet.LibraryModel;
using NuGet.Packaging;
using NuGet.ProjectModel;
using NuGet.Repositories;
using NuGet.Shared;
using NuGetVersion = NuGet.Versioning.NuGetVersion;

namespace NuGet.Commands
{
    public class LockFileBuilder
    {
        private readonly int _lockFileVersion;
        private readonly ILogger _logger;
        private readonly Dictionary<RestoreTargetGraph, Dictionary<string, LibraryIncludeFlags>> _includeFlagGraphs;

        public LockFileBuilder(int lockFileVersion,
            ILogger logger,
            Dictionary<RestoreTargetGraph,
            Dictionary<string, LibraryIncludeFlags>> includeFlagGraphs)
        {
            _lockFileVersion = lockFileVersion;
            _logger = logger;
            _includeFlagGraphs = includeFlagGraphs;
        }

        public LockFile CreateLockFile(LockFile previousLockFile,
            PackageSpec project,
            IEnumerable<RestoreTargetGraph> targetGraphs,
            IReadOnlyList<NuGetv3LocalRepository> localRepositories,
            RemoteWalkContext context,
            LockFileBuilderCache lockFileBuilderCache)
        {
            var lockFile = new LockFile()
            {
                Version = _lockFileVersion
            };

            var previousLibraries = previousLockFile?.Libraries.ToDictionary(l => ValueTuple.Create(l.Name, l.Version));

            if (project.RestoreMetadata?.ProjectStyle == ProjectStyle.PackageReference)
            {
                AddProjectFileDependenciesForPackageReference(project, lockFile, targetGraphs.AsList());
            }

            // Record all libraries used
            var libraryItems = targetGraphs
                .SelectMany(g => g.Flattened) // All GraphItem<RemoteResolveResult> resolved in the graph.
                .Distinct(GraphItemKeyComparer<RemoteResolveResult>.Instance) // Distinct list of GraphItems. Two items are equal only if the itmes' Keys are equal.
                .OrderBy(x => x.Data.Match.Library);
            foreach (var item in libraryItems)
            {
                var library = item.Data.Match.Library;

                if (project.Name.Equals(library.Name, StringComparison.OrdinalIgnoreCase))
                {
                    // Do not include the project itself as a library.
                    continue;
                }

                if (library.Type == LibraryType.Project || library.Type == LibraryType.ExternalProject)
                {
                    // Project
                    var localMatch = (LocalMatch)item.Data.Match;
                    string path = null;
                    string msBuildProject = null;

                    // Set the relative path if a path exists
                    // For projects without project.json this will be empty
                    if (!string.IsNullOrEmpty(localMatch.LocalLibrary.Path))
                    {
                        path = PathUtility.GetRelativePath(
                            project.FilePath,
                            localMatch.LocalLibrary.Path,
                            '/');
                    }

                    // The msbuild project path if it exists
                    object msbuildPath;
                    if (localMatch.LocalLibrary.Items.TryGetValue(KnownLibraryProperties.MSBuildProjectPath, out msbuildPath))
                    {
                        var msbuildRelativePath = PathUtility.GetRelativePath(
                            project.FilePath,
                            (string)msbuildPath,
                            '/');

                        msBuildProject = msbuildRelativePath;
                    }

                    var projectLib = new LockFileLibrary()
                    {
                        MSBuildProject = msBuildProject,
                        Name = library.Name,
                        Path = path,
                        Version = library.Version,
                        Type = LibraryType.Project,
                    };

                    lockFile.Libraries.Add(projectLib);
                }
                else if (library.Type == LibraryType.Package)
                {
                    // Packages
                    var packageInfo = NuGetv3LocalRepositoryUtility.GetPackage(localRepositories, library.Name, library.Version);

                    // Add the library if it was resolved, unresolved packages are not added to the assets file.
                    if (packageInfo != null)
                    {
                        var package = packageInfo.Package;
                        var resolver = packageInfo.Repository.PathResolver;

                        var sha512 = package.Sha512;
                        var path = PathUtility.GetPathWithForwardSlashes(resolver.GetPackageDirectory(package.Id, package.Version));
                        LockFileLibrary lockFileLib = null;
                        LockFileLibrary previousLibrary = null;

                        if (previousLibraries?.TryGetValue(ValueTuple.Create(package.Id, package.Version), out previousLibrary) == true)
                        {
                            // Check that the previous library is still valid
                            if (previousLibrary != null
                                && StringComparer.Ordinal.Equals(path, previousLibrary.Path)
                                && StringComparer.Ordinal.Equals(sha512, previousLibrary.Sha512))
                            {
                                // When deciding whether the lock file has changed,
                                // we compare the new lock file to the previous (in-memory) lock file.
                                lockFileLib = previousLibrary;
                            }
                        }

                        // Create a new lock file library if one doesn't exist already.
                        if (lockFileLib == null)
                        {
                            lockFileLib = CreateLockFileLibrary(package, sha512, path);
                        }

                        // Create a new lock file library
                        lockFile.Libraries.Add(lockFileLib);
                    }
                }
            }

            Dictionary<ValueTuple<string, NuGetVersion>, LockFileLibrary> libraries = EnsureUniqueLockFileLibraries(lockFile);

            var librariesWithWarnings = new HashSet<LibraryIdentity>();

            var rootProjectStyle = project.RestoreMetadata?.ProjectStyle ?? ProjectStyle.Unknown;

            // Add the targets
            foreach (var targetGraph in targetGraphs
                .OrderBy(graph => graph.Framework.ToString(), StringComparer.Ordinal)
                .ThenBy(graph => graph.RuntimeIdentifier, StringComparer.Ordinal))
            {
                var librariesWithMonoAndroidWarnings = new HashSet<LibraryIdentity>();

                var target = lockFile.Version >= LockFileFormat.AliasedVersion ?
                    new LockFileTarget
                    {
                        TargetFramework = targetGraph.Framework,
                        RuntimeIdentifier = targetGraph.RuntimeIdentifier,
                        TargetAlias = targetGraph.TargetAlias,
                        Name = targetGraph.TargetGraphName
                    } :
                    new LockFileTarget
                    {
                        TargetFramework = targetGraph.Framework,
                        RuntimeIdentifier = targetGraph.RuntimeIdentifier,
                        TargetAlias = targetGraph.TargetAlias,
                    };

                var flattenedFlags = IncludeFlagUtils.FlattenDependencyTypes(_includeFlagGraphs, project, targetGraph);

                // Check if warnings should be displayed for the current framework.
                var tfi = project.GetTargetFramework(targetGraph.Framework);

                bool warnForImportsOnGraph = tfi.Warn
                    && (target.TargetFramework is FallbackFramework
                        || target.TargetFramework is AssetTargetFallbackFramework);

                bool checkMonoAndroidDeprecation = MonoAndroidDeprecation.ShouldCheck(project, targetGraph.Framework);

                foreach (var graphItem in targetGraph.Flattened.OrderBy(x => x.Key))
                {
                    var library = graphItem.Key;

                    // include flags
                    LibraryIncludeFlags includeFlags;
                    if (!flattenedFlags.TryGetValue(library.Name, out includeFlags))
                    {
                        includeFlags = ~LibraryIncludeFlags.ContentFiles;
                    }

                    if (library.Type == LibraryType.Project || library.Type == LibraryType.ExternalProject)
                    {
                        if (project.Name.Equals(library.Name, StringComparison.OrdinalIgnoreCase))
                        {
                            // Do not include the project itself as a library.
                            continue;
                        }

                        var projectLib = LockFileUtils.CreateLockFileTargetProject(
                            graphItem,
                            library,
                            includeFlags,
                            targetGraph,
                            rootProjectStyle);

                        target.Libraries.Add(projectLib);
                    }
                    else if (library.Type == LibraryType.Package)
                    {
                        var packageInfo = NuGetv3LocalRepositoryUtility.GetPackage(localRepositories, library.Name, library.Version);

                        if (packageInfo == null)
                        {
                            continue;
                        }

                        var package = packageInfo.Package;
                        var libraryDependency = tfi.Dependencies.FirstOrDefault(e => e.Name.Equals(library.Name, StringComparison.OrdinalIgnoreCase));

                        (LockFileTargetLibrary targetLibrary, bool usedFallbackFramework, NuGetFramework compileAssetFramework, NuGetFramework runtimeAssetFramework) = LockFileUtils.CreateLockFileTargetLibrary(
                            libraryDependency?.Aliases,
                            libraries[ValueTuple.Create(library.Name, library.Version)],
                            package,
                            targetGraph,
                            dependencyType: includeFlags,
                            targetFrameworkOverride: null,
                            dependencies: graphItem.Data.Dependencies,
                            cache: lockFileBuilderCache);

                        target.Libraries.Add(targetLibrary);

                        // Log warnings if the target library used the fallback framework
                        if (warnForImportsOnGraph && !librariesWithWarnings.Contains(library))
                        {
                            if (target.TargetFramework is FallbackFramework)
                            {
                                // PackageTargetFallback works different from AssetTargetFallback so the warning logic for PTF cannot be optimized.
                                var nonFallbackFramework = new NuGetFramework(target.TargetFramework);

                                var targetLibraryWithoutFallback = LockFileUtils.CreateLockFileTargetLibrary(
                                    libraryDependency?.Aliases,
                                    libraries[ValueTuple.Create(library.Name, library.Version)],
                                    package,
                                    targetGraph,
                                    targetFrameworkOverride: nonFallbackFramework,
                                    dependencyType: includeFlags,
                                    dependencies: graphItem.Data.Dependencies,
                                    cache: lockFileBuilderCache);
                                usedFallbackFramework = !targetLibrary.Equals(targetLibraryWithoutFallback);
                            }

                            if (usedFallbackFramework)
                            {
                                var libraryName = DiagnosticUtility.FormatIdentity(library);

                                var message = string.Format(CultureInfo.CurrentCulture,
                                    Strings.Log_ImportsFallbackWarning,
                                    libraryName,
                                    GetFallbackFrameworkString(target.TargetFramework),
                                    new NuGetFramework(target.TargetFramework));

                                var logMessage = RestoreLogMessage.CreateWarning(
                                    NuGetLogCode.NU1701,
                                    message,
                                    library.Name,
                                    targetGraph.TargetGraphName);

                                _logger.Log(logMessage);

                                // only log the warning once per library
                                librariesWithWarnings.Add(library);
                            }
                        }

                        // Log NU1703 warning if the package uses the deprecated MonoAndroid framework
                        if (checkMonoAndroidDeprecation
                            && !librariesWithMonoAndroidWarnings.Contains(library)
                            && (MonoAndroidDeprecation.IsMonoAndroidFramework(compileAssetFramework)
                                || MonoAndroidDeprecation.IsMonoAndroidFramework(runtimeAssetFramework)))
                        {
                            var message = string.Format(CultureInfo.CurrentCulture,
                                Strings.Warning_MonoAndroidFrameworkDeprecated,
                                library.Name,
                                library.Version);

                            var logMessage = RestoreLogMessage.CreateWarning(
                                NuGetLogCode.NU1703,
                                message,
                                library.Name,
                                targetGraph.TargetGraphName);

                            _logger.Log(logMessage);

                            // only log the warning once per library
                            librariesWithMonoAndroidWarnings.Add(library);
                        }
                    }
                }

                EnsureUniqueLockFileTargetLibraries(target);
                lockFile.Targets.Add(target);
            }

            PopulatePackageFolders(localRepositories.Select(repo => repo.RepositoryRoot).Distinct(), lockFile);

            AddCentralTransitiveDependencyGroupsForPackageReference(project, lockFile, targetGraphs, _logger);

            // Add the original package spec to the lock file.
            lockFile.PackageSpec = project;

            return lockFile;
        }

        private Dictionary<ValueTuple<string, NuGetVersion>, LockFileLibrary> EnsureUniqueLockFileLibraries(LockFile lockFile)
        {
            IList<LockFileLibrary> libraries = lockFile.Libraries;
            var libraryReferences = new Dictionary<ValueTuple<string, NuGetVersion>, LockFileLibrary>();

            foreach (LockFileLibrary lib in libraries)
            {
                var libraryKey = ValueTuple.Create(lib.Name, lib.Version);

                if (libraryReferences.TryGetValue(libraryKey, out LockFileLibrary existingLibrary))
                {
                    if (RankReferences(existingLibrary.Type) > RankReferences(lib.Type))
                    {
                        // Prefer project reference over package reference, so replace the the package reference.
                        libraryReferences[libraryKey] = lib;
                    }
                }
                else
                {
                    libraryReferences[libraryKey] = lib;
                }
            }

            if (lockFile.Libraries.Count != libraryReferences.Count)
            {
                lockFile.Libraries = new List<LockFileLibrary>(libraryReferences.Count);
                foreach (KeyValuePair<ValueTuple<string, NuGetVersion>, LockFileLibrary> pair in libraryReferences)
                {
                    lockFile.Libraries.Add(pair.Value);
                }
            }

            return libraryReferences;
        }

        private static void EnsureUniqueLockFileTargetLibraries(LockFileTarget lockFileTarget)
        {
            IList<LockFileTargetLibrary> libraries = lockFileTarget.Libraries;
            var libraryReferences = new Dictionary<LockFileTargetLibrary, LockFileTargetLibrary>(comparer: LockFileTargetLibraryNameAndVersionEqualityComparer.Instance);

            foreach (LockFileTargetLibrary library in libraries)
            {
                if (libraryReferences.TryGetValue(library, out LockFileTargetLibrary existingLibrary))
                {
                    if (RankReferences(existingLibrary.Type) > RankReferences(library.Type))
                    {
                        // Prefer project reference over package reference, so replace the the package reference.
                        libraryReferences[library] = library;
                    }
                }
                else
                {
                    libraryReferences[library] = library;
                }
            }

            if (lockFileTarget.Libraries.Count == libraryReferences.Count)
            {
                return;
            }

            lockFileTarget.Libraries = new List<LockFileTargetLibrary>(libraryReferences.Count);
            foreach (KeyValuePair<LockFileTargetLibrary, LockFileTargetLibrary> pair in libraryReferences)
            {
                lockFileTarget.Libraries.Add(pair.Value);
            }
        }

        /// <summary>
        /// Prefer projects over packages
        /// </summary>
        /// <param name="referenceType"></param>
        /// <returns></returns>
        private static int RankReferences(string referenceType)
        {
            if (referenceType == "project")
            {
                return 0;
            }
            else if (referenceType == "externalProject")
            {
                return 1;
            }
            else if (referenceType == "package")
            {
                return 2;
            }

            return 5;
        }

        private static string GetFallbackFrameworkString(NuGetFramework framework)
        {
            var frameworks = (framework as AssetTargetFallbackFramework)?.Fallback
                ?? (framework as FallbackFramework)?.Fallback
                ?? new List<NuGetFramework>();

            return string.Join(", ", frameworks);
        }

        private static void AddProjectFileDependenciesForPackageReference(PackageSpec project, LockFile lockFile, List<RestoreTargetGraph> targetGraphs)
        {
            bool isAliasedLockFile = lockFile.Version >= LockFileFormat.AliasedVersion;

            foreach (var frameworkInfo in project.TargetFrameworks
                .OrderBy(framework => framework.TargetAlias,
                    StringComparer.Ordinal))
            {
                var dependencies = new List<LibraryRange>();
                dependencies.AddRange(frameworkInfo.Dependencies.Select(e => e.LibraryRange));

                RestoreTargetGraph targetGraph = !string.IsNullOrEmpty(frameworkInfo.TargetAlias) ?
                    targetGraphs.SingleOrDefault(graph =>
                        frameworkInfo.TargetAlias.Equals(graph.TargetAlias)
                        && string.IsNullOrEmpty(graph.RuntimeIdentifier)) :
                    targetGraphs.SingleOrDefault(graph =>
                        graph.Framework.Equals(frameworkInfo.FrameworkName)
                        && string.IsNullOrEmpty(graph.RuntimeIdentifier));

                var resolvedEntry = targetGraph?
                    .Flattened
                    .SingleOrDefault(library => library.Key.Name.Equals(project.Name, StringComparison.OrdinalIgnoreCase));

                // In some failure cases where there is a conflict the root level project cannot be resolved, this should be handled gracefully
                if (resolvedEntry != null)
                {
                    dependencies.AddRange(resolvedEntry.Data.Dependencies.Where(lib =>
                        lib.LibraryRange.TypeConstraint == LibraryDependencyTarget.ExternalProject)
                        .Select(lib => lib.LibraryRange));
                }

                var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
                var uniqueDependencies = new List<LibraryRange>();

                foreach (var dependency in dependencies)
                {
                    if (seen.Add(dependency.Name))
                    {
                        uniqueDependencies.Add(dependency);
                    }
                }

                // Add entry
                string framework = (isAliasedLockFile && !string.IsNullOrEmpty(frameworkInfo.TargetAlias)) ? frameworkInfo.TargetAlias : frameworkInfo.FrameworkName.ToString();
                var dependencyGroup = new ProjectFileDependencyGroup(
                    framework,
                    uniqueDependencies.Select(x => x.ToLockFileDependencyGroupString())
                        .OrderBy(dependency => dependency, StringComparer.Ordinal));

                lockFile.ProjectFileDependencyGroups.Add(dependencyGroup);
            }
        }

        private void AddCentralTransitiveDependencyGroupsForPackageReference(PackageSpec project, LockFile lockFile, IEnumerable<RestoreTargetGraph> targetGraphs, ILogger logger)
        {
            if (project.RestoreMetadata == null || !project.RestoreMetadata.CentralPackageVersionsEnabled || !project.RestoreMetadata.CentralPackageTransitivePinningEnabled)
            {
                return;
            }

            // Do not pack anything from the runtime graphs
            // The runtime graphs are added in addition to the graphs without a runtime
            foreach (RestoreTargetGraph targetGraph in targetGraphs.Where(targetGraph => string.IsNullOrEmpty(targetGraph.RuntimeIdentifier)))
            {
                TargetFrameworkInformation targetFrameworkInformation = project.TargetFrameworks.FirstOrDefault(i => i.FrameworkName.Equals(targetGraph.Framework));

                if (targetFrameworkInformation == null)
                {
                    continue;
                }

                // The transitive dependencies enforced by the central package version management file are written to the assets to be used by the pack task.
                List<LibraryDependency> centralEnforcedTransitiveDependencies = GetLibraryDependenciesForCentralTransitiveDependencies(targetGraph, targetFrameworkInformation, logger).ToList();

                if (centralEnforcedTransitiveDependencies.Any())
                {
                    var centralEnforcedTransitiveDependencyGroup = new CentralTransitiveDependencyGroup
                    (
                        targetGraph.Framework,
                        centralEnforcedTransitiveDependencies
                    );

                    lockFile.CentralTransitiveDependencyGroups.Add(centralEnforcedTransitiveDependencyGroup);
                }
            }
        }

        /// <summary>
        /// Determines the <see cref="LibraryDependency" /> objects for the specified <see cref="RestoreTargetGraph" /> that represent the centrally defined transitive dependencies.
        /// </summary>
        /// <param name="targetGraph">The <see cref="RestoreTargetGraph" /> to get centrally defined transitive dependencies for.</param>
        /// <param name="targetFrameworkInformation">The <see cref="TargetFrameworkInformation" /> for the target framework to get centrally defined transitive dependencies for.</param>
        /// <param name="logger">An <see cref="ILogger" /> to use for logging.</param>
        /// <returns>An <see cref="IEnumerable{LibraryDependency}" /> representing the centrally defined transitive dependencies for the specified <see cref="RestoreTargetGraph" />.</returns>
        private IEnumerable<LibraryDependency> GetLibraryDependenciesForCentralTransitiveDependencies(RestoreTargetGraph targetGraph, TargetFrameworkInformation targetFrameworkInformation, ILogger logger)
        {
            HashSet<GraphNode<RemoteResolveResult>> visitedNodes = new HashSet<GraphNode<RemoteResolveResult>>();
            Queue<GraphNode<RemoteResolveResult>> queue = new Queue<GraphNode<RemoteResolveResult>>();

            foreach (GraphNode<RemoteResolveResult> rootNode in targetGraph.Graphs)
            {
                Dictionary<string, LibraryDependency> dependencyDictionary = null;

                foreach (GraphNode<RemoteResolveResult> node in rootNode.InnerNodes)
                {
                    // Only consider nodes that are not unresolved, Accepted, IsCentralTransitive, and have a centrally defined package version
                    if (node?.Item == null
                        || node.Item.Key.Type == LibraryType.Unresolved
                        || node.Disposition != Disposition.Accepted
                        || !node.Item.IsCentralTransitive
                        || !targetFrameworkInformation.CentralPackageVersions?.ContainsKey(node.Item.Key.Name) == true)
                    {
                        continue;
                    }

                    if (dependencyDictionary == null)
                    {
                        dependencyDictionary = rootNode.Item.Data.Dependencies.ToDictionary(x => x.Name, x => x, StringComparer.OrdinalIgnoreCase);
                    }

                    CentralPackageVersion centralPackageVersion = targetFrameworkInformation.CentralPackageVersions[node.Item.Key.Name];
                    Dictionary<string, LibraryIncludeFlags> dependenciesIncludeFlags = _includeFlagGraphs[targetGraph];

                    LibraryIncludeFlags suppressParent = LibraryIncludeFlags.All;

                    // Centrally pinned dependencies are not directly declared but the intersection of all of the PrivateAssets of the parents that pulled it in should apply to it
                    foreach (GraphNode<RemoteResolveResult> dependencyNode in EnumerateNodesForDependencyChecks(visitedNodes, queue, rootNode, node))
                    {
                        LibraryDependency dependency = dependencyDictionary[dependencyNode.Key.Name];
                        suppressParent &= dependency.SuppressParent;
                    }

                    // If all assets are suppressed then the dependency should not be added
                    if (suppressParent == LibraryIncludeFlags.All)
                    {
                        continue;
                    }

                    if (!dependenciesIncludeFlags.TryGetValue(centralPackageVersion.Name, out LibraryIncludeFlags includeType))
                    {
                        // This should never happen, if the package was resolved there should be a calculated include type for it
                        // There have been bugs in the past where getting a value from the dictionary was throwing a KeyNotFoundException
                        // Log an error with the info needed asking the user to file an issue
                        logger.Log(
                            LogMessage.CreateError(
                                NuGetLogCode.NU1000,
                                string.Format(
                                    CultureInfo.CurrentCulture,
                                    Strings.Error_CentralPackageManagement_MissingTransitivelyPinnedIncludeType,
                                    centralPackageVersion.Name,
                                    node.Item.Key)));

                        continue;
                    }

                    yield return new LibraryDependency()
                    {
                        LibraryRange = new LibraryRange(centralPackageVersion.Name, centralPackageVersion.VersionRange, LibraryDependencyTarget.Package),
                        ReferenceType = LibraryDependencyReferenceType.Transitive,
                        VersionCentrallyManaged = true,
                        IncludeType = includeType,
                        SuppressParent = suppressParent,
                    };
                }
            }
        }

        /// <summary>
        /// Enumerates all inner nodes of the root node which directly or transitively reference the particular graph node.
        /// </summary>
        /// <typeparam name="T">The type of the node.</typeparam>
        /// <param name="visitedNodes">Reusable <see cref="HashSet{GraphNode{T}}" /> for graph traversal algorithm.</param>
        /// <param name="queue">Reusable <see cref="Queue{GraphNode{T}}" /> for graph traversal algorithm.</param>
        /// <param name="rootNode">The <see cref="GraphNode{TItem}" /> to know which nodes are first level inner nodes.</param>
        /// <param name="graphNode">The <see cref="GraphNode{TItem}" /> to enumerate the parent nodes of.</param>
        /// <returns>An <see cref="IEnumerable{T}" /> containing list of parent nodes of the specified node.</returns>
        private static IEnumerable<GraphNode<T>> EnumerateNodesForDependencyChecks<T>(HashSet<GraphNode<T>> visitedNodes, Queue<GraphNode<T>> queue, GraphNode<T> rootNode, GraphNode<T> graphNode)
        {
            visitedNodes.Clear();
            queue.Clear();

            queue.Enqueue(graphNode);

            while (queue.Count > 0)
            {
                var node = queue.Dequeue();
                if (!visitedNodes.Add(node))
                    continue;

                if (node.Item.IsCentralTransitive)
                {
                    foreach (var parentNode in node.ParentNodes)
                    {
                        queue.Enqueue(parentNode);
                    }
                }
                else
                {
                    if (node.OuterNode == rootNode)
                    {
                        // We were traversing the graph upwards and now found a parent node (an inner node of the root node).
                        // Now we return it, so it can be used to calculate the effective value of SuppressParent for the initial node.
                        yield return node;
                    }
                    else
                    {
                        // Go one level up
                        queue.Enqueue(node.OuterNode);
                    }
                }
            }
        }

        private static void PopulatePackageFolders(IEnumerable<string> packageFolders, LockFile lockFile)
        {
            lockFile.PackageFolders.AddRange(packageFolders.Select(path => new LockFileItem(path)));
        }

        internal static LockFileLibrary CreateLockFileLibrary(LocalPackageInfo package, string sha512, string path)
        {
            var hasTools = false;

            // Use for loop to avoid boxing enumerator
            for (var i = 0; i < package.Files.Count; i++)
            {
                if (HasTools(package.Files[i]))
                {
                    hasTools = true;
                    break;
                }
            }

            // This should avoid allocating a new array as package.Files should be a boxed ImmutableArray<string>
            var files = package.Files as IList<string> ?? package.Files.ToImmutableArray();

            var lockFileLib = new LockFileLibrary
            {
                Files = files,
                HasTools = hasTools,
                Name = package.Id,
                Version = package.Version,
                Type = LibraryType.Package,
                Sha512 = sha512,

                // This is the relative path, appended to the global packages folder path. All
                // of the paths in the in the Files property should be appended to this path along
                // with the global packages folder path to get the absolute path to each file in the
                // package.
                Path = path
            };

            return lockFileLib;
        }

        private static bool HasTools(string file)
        {
            return file.StartsWith("tools/", StringComparison.OrdinalIgnoreCase);
        }

        /// <summary>
        /// An <see cref="IEqualityComparer{T}" /> that compares <see cref="LockFileTargetLibrary" /> objects by the value of the <see cref="LockFileTargetLibrary.Name" /> and <see cref="LockFileTargetLibrary.Version" /> properties.
        /// </summary>
        private class LockFileTargetLibraryNameAndVersionEqualityComparer : IEqualityComparer<LockFileTargetLibrary>
        {
            /// <summary>
            /// Gets a static singleton for the <see cref="LockFileTargetLibraryNameAndVersionEqualityComparer" /> class.
            /// </summary>
            public static LockFileTargetLibraryNameAndVersionEqualityComparer Instance = new();

            /// <summary>
            /// Initializes a new instance of the <see cref="LockFileTargetLibraryNameAndVersionEqualityComparer" /> class.
            /// </summary>
            private LockFileTargetLibraryNameAndVersionEqualityComparer()
            {
            }

            /// <summary>
            /// Determines whether the specified <see cref="LockFileTargetLibrary" /> objects are equal by comparing their <see cref="LockFileTargetLibrary.Name" /> and <see cref="LockFileTargetLibrary.Version" /> properties.
            /// </summary>
            /// <param name="x">The first <see cref="LockFileTargetLibrary" /> to compare.</param>
            /// <param name="y">The second <see cref="LockFileTargetLibrary" /> to compare.</param>
            /// <returns><see langword="true" /> if the specified <see cref="LockFileTargetLibrary" /> objects' <see cref="LockFileTargetLibrary.Name" /> and <see cref="LockFileTargetLibrary.Version" /> properties are equal, otherwise <see langword="false" />.</returns>
            public bool Equals(LockFileTargetLibrary x, LockFileTargetLibrary y)
            {
                return string.Equals(x.Name, y.Name, StringComparison.Ordinal) && x.Version.Equals(y.Version);
            }

            /// <summary>
            /// Returns a hash code for the specified <see cref="LockFileTargetLibrary" /> object's <see cref="LockFileTargetLibrary.Name" /> property.
            /// </summary>
            /// <param name="obj">The <see cref="LockFileTargetLibrary" /> for which a hash code is to be returned.</param>
            /// <returns>A hash code for the specified <see cref="LockFileTargetLibrary" /> object's <see cref="LockFileTargetLibrary.Name" /> and and <see cref="LockFileTargetLibrary.Version" /> properties.</returns>
            public int GetHashCode(LockFileTargetLibrary obj)
            {
                var combiner = new HashCodeCombiner();

                combiner.AddObject(obj.Name);
                combiner.AddObject(obj.Version);

                return combiner.CombinedHash;
            }
        }
    }
}