File: PackageSpecReferenceDependencyProvider.cs
Web Access
Project: src\src\nuget-client\src\NuGet.Core\NuGet.ProjectModel\NuGet.ProjectModel.csproj (NuGet.ProjectModel)
// 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.Diagnostics;
using System.Linq;
using NuGet.Common;
using NuGet.DependencyResolver;
using NuGet.Frameworks;
using NuGet.LibraryModel;
using NuGet.Versioning;

namespace NuGet.ProjectModel
{
    /// <summary>
    /// Handles both external references and projects discovered through directories
    /// If the type is set to external project directory discovery will be disabled.
    /// </summary>
    public class PackageSpecReferenceDependencyProvider : IDependencyProvider
    {
        private readonly Dictionary<string, ExternalProjectReference> _externalProjectsByPath
            = new Dictionary<string, ExternalProjectReference>(StringComparer.OrdinalIgnoreCase);

        private readonly Dictionary<string, ExternalProjectReference> _externalProjectsByUniqueName
            = new Dictionary<string, ExternalProjectReference>(StringComparer.OrdinalIgnoreCase);

        private readonly bool _useLegacyAssetTargetFallbackBehavior;

        private readonly bool _useLegacyDependencyGraphResolution = false;

        public PackageSpecReferenceDependencyProvider(
            IEnumerable<ExternalProjectReference> externalProjects,
            ILogger logger) :
            this(externalProjects,
                environmentVariableReader: EnvironmentVariableWrapper.Instance,
                useLegacyDependencyGraphResolution: false)
        {
        }

        public PackageSpecReferenceDependencyProvider(
            IEnumerable<ExternalProjectReference> externalProjects,
            ILogger logger,
            bool useLegacyDependencyGraphResolution) :
            this(externalProjects,
                environmentVariableReader: EnvironmentVariableWrapper.Instance,
                useLegacyDependencyGraphResolution)
        {
        }

        internal PackageSpecReferenceDependencyProvider(
            IEnumerable<ExternalProjectReference> externalProjects,
            IEnvironmentVariableReader environmentVariableReader,
            bool useLegacyDependencyGraphResolution = false)
        {
            if (externalProjects == null)
            {
                throw new ArgumentNullException(nameof(externalProjects));
            }

            foreach (var project in externalProjects)
            {
                Debug.Assert(
                    !_externalProjectsByPath.ContainsKey(project.UniqueName),
                    $"Duplicate project {project.UniqueName}");

                if (!_externalProjectsByPath.ContainsKey(project.UniqueName))
                {
                    _externalProjectsByPath.Add(project.UniqueName, project);
                }

                Debug.Assert(
                    !_externalProjectsByUniqueName.ContainsKey(project.ProjectName),
                    $"Duplicate project {project.ProjectName}");

                if (!_externalProjectsByUniqueName.ContainsKey(project.ProjectName))
                {
                    _externalProjectsByUniqueName.Add(project.ProjectName, project);
                }

                if (!_externalProjectsByUniqueName.ContainsKey(project.UniqueName))
                {
                    _externalProjectsByUniqueName.Add(project.UniqueName, project);
                }
            }
            _useLegacyAssetTargetFallbackBehavior = MSBuildStringUtility.IsTrue(environmentVariableReader.GetEnvironmentVariable("NUGET_USE_LEGACY_ASSET_TARGET_FALLBACK_DEPENDENCY_RESOLUTION"));
            _useLegacyDependencyGraphResolution = useLegacyDependencyGraphResolution;
        }

        public bool SupportsType(LibraryDependencyTarget libraryType)
        {
            return (libraryType & (LibraryDependencyTarget.Project | LibraryDependencyTarget.ExternalProject)) != LibraryDependencyTarget.None;
        }

        [Obsolete("This method is obsolete and will be removed in future versions. Use GetLibrary(LibraryRange, NuGetFramework, string) instead.")]
        public Library GetLibrary(LibraryRange libraryRange, NuGetFramework targetFramework)
        {
            return GetLibrary(libraryRange, targetFramework, alias: null);
        }

        public Library GetLibrary(LibraryRange libraryRange, NuGetFramework targetFramework, string alias)
        {
            if (libraryRange == null) throw new ArgumentNullException(nameof(libraryRange));
            if (targetFramework == null) throw new ArgumentNullException(nameof(targetFramework));

            PackageSpec packageSpec = null;

            // This must exist in the external references
            if (_externalProjectsByUniqueName.TryGetValue(libraryRange.Name, out ExternalProjectReference externalReference))
            {
                packageSpec = externalReference.PackageSpec;
            }

            if (externalReference == null && packageSpec == null)
            {
                // unable to find any projects
                return null;
            }

            List<LibraryDependency> dependencies;

            var projectStyle = packageSpec?.RestoreMetadata?.ProjectStyle ?? ProjectStyle.Unknown;

            if (projectStyle == ProjectStyle.PackageReference ||
                projectStyle == ProjectStyle.DotnetCliTool)
            {
                dependencies = GetDependenciesFromSpecRestoreMetadata(packageSpec, targetFramework, alias);
            }
            else
            {
                dependencies = GetDependenciesFromExternalReference(externalReference);
            }

            List<LibraryDependency> uniqueDependencies = DeduplicateDependencies(dependencies);

            Library library = new Library
            {
                LibraryRange = libraryRange,
                Identity = new LibraryIdentity
                {
                    Name = externalReference?.ProjectName ?? packageSpec.Name,
                    Version = packageSpec?.Version ?? new NuGetVersion(1, 0, 0),
                    Type = LibraryType.Project,
                },
                Path = packageSpec?.FilePath,
                Dependencies = uniqueDependencies,
                Resolved = true
            };

            // Add msbuild path
            var msbuildPath = externalReference?.MSBuildProjectPath;
            if (msbuildPath != null)
            {
                library[KnownLibraryProperties.MSBuildProjectPath] = msbuildPath;
            }

            if (packageSpec != null)
            {
                // Additional library properties
                AddLibraryProperties(library, packageSpec, targetFramework, alias);
            }

            return library;

            static List<LibraryDependency> DeduplicateDependencies(List<LibraryDependency> dependencies)
            {
                if (dependencies.Count == 0)
                {
                    return dependencies;
                }
                // Remove duplicate dependencies.
                // dependencies is already ordered by importance here
                var uniqueDependencies = new List<LibraryDependency>(dependencies.Count);
                var projectNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

                foreach (var project in dependencies)
                {
                    if (projectNames.Add(project.Name))
                    {
                        uniqueDependencies.Add(project);
                    }
                }

                return uniqueDependencies;
            }
        }

        private static void AddLibraryProperties(Library library, PackageSpec packageSpec, NuGetFramework targetFramework, string alias)
        {
            var projectStyle = packageSpec.RestoreMetadata?.ProjectStyle ?? ProjectStyle.Unknown;

            library[KnownLibraryProperties.PackageSpec] = packageSpec;
            library[KnownLibraryProperties.ProjectStyle] = projectStyle;

            if (packageSpec.RestoreMetadata?.Files != null)
            {
                // Record all files that would be in a nupkg
                library[KnownLibraryProperties.ProjectRestoreMetadataFiles]
                    = packageSpec.RestoreMetadata.Files.ToList();
            }

            // Avoid adding these properties for class libraries
            // and other projects which are not fully able to
            // participate in restore.
            if (packageSpec.RestoreMetadata == null
                || (projectStyle != ProjectStyle.Unknown
                    && projectStyle != ProjectStyle.PackagesConfig))
            {
                var frameworks = new List<NuGetFramework>(
                    packageSpec.TargetFrameworks.Select(fw => fw.FrameworkName)
                    .Where(fw => !fw.IsUnsupported));

                // Record all frameworks in the project
                library[KnownLibraryProperties.ProjectFrameworks] = frameworks;

                var targetFrameworkInfo = GetNearestTargetFrameworkWithFallbacks(packageSpec, targetFramework, alias);

                library[KnownLibraryProperties.TargetFrameworkInformation] = targetFrameworkInfo;

                // Add framework assemblies
                var frameworkAssemblies = targetFrameworkInfo.Dependencies
                    .Where(d => d.LibraryRange.TypeConstraint == LibraryDependencyTarget.Reference)
                    .Select(d => d.Name)
                    .Distinct(StringComparer.OrdinalIgnoreCase)
                    .ToList();

                library[KnownLibraryProperties.FrameworkAssemblies] = frameworkAssemblies;

                // Add framework references
                library[KnownLibraryProperties.FrameworkReferences] = targetFrameworkInfo.FrameworkReferences;
            }
        }

        private List<LibraryDependency> GetDependenciesFromSpecRestoreMetadata(PackageSpec packageSpec, NuGetFramework targetFramework, string targetAlias)
        {
            var targetFrameworkInfo = packageSpec.GetNearestTargetFramework(targetFramework, targetAlias);

            if (!_useLegacyAssetTargetFallbackBehavior)
            {
                targetFrameworkInfo = GetNearestTargetFrameworkWithFallbacks(packageSpec, targetFramework, targetAlias, targetFrameworkInfo);
            }

            if (targetFrameworkInfo.FrameworkName == null)
            {
                return [];
            }

            List<LibraryDependency> dependencies = GetSpecDependencies(packageSpec, targetFrameworkInfo);
            ProjectRestoreMetadataFrameworkInfo referencesForFramework = packageSpec.GetRestoreMetadataFramework(targetFrameworkInfo.TargetAlias);

            // Ensure that this project is compatible
            if (referencesForFramework?.FrameworkName?.IsSpecificFramework == true)
            {
                foreach (var reference in referencesForFramework.ProjectReferences)
                {
                    // Defaults, these will be used for missing projects
                    var dependencyName = reference.ProjectPath;
                    var range = VersionRange.All;

                    ExternalProjectReference externalProject;
                    if (_externalProjectsByUniqueName.TryGetValue(reference.ProjectUniqueName, out externalProject))
                    {
                        dependencyName = externalProject.ProjectName;

                        // Create a version range based on the project if it has a range
                        var version = externalProject.PackageSpec?.Version;
                        if (version != null)
                        {
                            range = new VersionRange(version);
                        }
                    }

                    var dependency = new LibraryDependency()
                    {
                        IncludeType = (reference.IncludeAssets & ~reference.ExcludeAssets),
                        SuppressParent = reference.PrivateAssets,
                        LibraryRange = new LibraryRange(
                            dependencyName,
                            range,
                            LibraryDependencyTarget.ExternalProject),
                    };

                    // Remove existing reference if one exists, projects override
                    dependencies.RemoveAll(e => StringComparer.OrdinalIgnoreCase.Equals(dependency.Name, e.Name));

                    // Add reference
                    dependencies.Add(dependency);
                }
            }

            return dependencies;
        }

        /// <summary>
        /// Resolves the nearest target framework with fallback precedence matching the package path:
        /// 1. Root framework (exact or nearest)
        /// 2. DualCompatibilityFramework secondary (e.g., native for C++/CLI)
        /// 3. AssetTargetFallback imports (e.g., net472)
        /// </summary>
        private static TargetFrameworkInformation GetNearestTargetFrameworkWithFallbacks(
            PackageSpec packageSpec,
            NuGetFramework targetFramework,
            string targetAlias,
            TargetFrameworkInformation initialResult = null)
        {
            var targetFrameworkInfo = initialResult ?? packageSpec.GetNearestTargetFramework(targetFramework, targetAlias);

            if (targetFrameworkInfo.FrameworkName != null)
            {
                return targetFrameworkInfo;
            }

            // Unwrap the root framework from ATF if present
            NuGetFramework rootFramework = targetFramework is AssetTargetFallbackFramework atf
                ? atf.RootFramework
                : targetFramework;

            // Try DualCompatibilityFramework secondary before ATF imports
            if (rootFramework is DualCompatibilityFramework dcf)
            {
                targetFrameworkInfo = packageSpec.GetNearestTargetFramework(dcf.AsFallbackFramework(), targetAlias);
                if (targetFrameworkInfo.FrameworkName != null)
                {
                    return targetFrameworkInfo;
                }
            }

            // Try ATF imports last
            if (targetFramework is AssetTargetFallbackFramework atfFramework)
            {
                targetFrameworkInfo = packageSpec.GetNearestTargetFramework(atfFramework.AsFallbackFramework(), targetAlias);
            }

            return targetFrameworkInfo;
        }

        private List<LibraryDependency> GetDependenciesFromExternalReference(ExternalProjectReference externalReference)
        {
            if (externalReference != null)
            {
                var childReferences = GetChildReferences(externalReference);
                var childReferenceNames = childReferences.Select(reference => reference.ProjectName).ToList();

                // External references are created without pivoting on the TxM. Here we need to account for this
                // and filter out references except the nearest TxM.
                var filteredExternalDependencies = new HashSet<string>(
                    childReferenceNames,
                    StringComparer.OrdinalIgnoreCase);

                return [.. childReferences
                    .Where(reference => filteredExternalDependencies.Contains(reference.ProjectName))
                    .Select(reference => new LibraryDependency()
                    {
                        LibraryRange = new LibraryRange
                        {
                            Name = reference.ProjectName,
                            VersionRange = VersionRange.Parse("1.0.0"),
                            TypeConstraint = LibraryDependencyTarget.ExternalProject
                        }
                    })];
            }

            return [];
        }

        private List<LibraryDependency> GetSpecDependencies(
            PackageSpec packageSpec,
            TargetFrameworkInformation targetFrameworkInfo)
        {
            List<LibraryDependency> dependencies = [.. targetFrameworkInfo.Dependencies];

            if (_useLegacyDependencyGraphResolution && packageSpec.RestoreMetadata?.CentralPackageVersionsEnabled == true &&
                packageSpec.RestoreMetadata?.CentralPackageTransitivePinningEnabled == true)
            {
                var dependencyNamesSet = new HashSet<string>(targetFrameworkInfo.Dependencies.Select(d => d.Name), StringComparer.OrdinalIgnoreCase);
                dependencies.AddRange(targetFrameworkInfo.CentralPackageVersions
                    .Where(item => !dependencyNamesSet.Contains(item.Key))
                    .Select(item => new LibraryDependency()
                    {
                        LibraryRange = new LibraryRange(item.Value.Name, item.Value.VersionRange, LibraryDependencyTarget.Package),
                        VersionCentrallyManaged = true,
                        ReferenceType = LibraryDependencyReferenceType.None,
                    }));
            }

            // Remove all framework assemblies
            dependencies.RemoveAll(d => d.LibraryRange.TypeConstraint == LibraryDependencyTarget.Reference);

            for (var i = 0; i < dependencies.Count; i++)
            {
                // Do not push the dependency changes here upwards, as the original package
                // spec should not be modified.

                // Remove "project" from the allowed types for this dependency
                // This will require that projects referenced by an msbuild project
                // must be external projects.
                var dependency = dependencies[i];
                bool isPruned = IsDependencyPruned(dependency, targetFrameworkInfo.PackagesToPrune);
                var libraryRange = new LibraryRange(dependency.LibraryRange) { TypeConstraint = dependency.LibraryRange.TypeConstraint & ~LibraryDependencyTarget.Project };
                dependencies[i] = new LibraryDependency(dependency)
                {
                    LibraryRange = libraryRange,
                    SuppressParent = isPruned ? LibraryIncludeFlags.All : dependency.SuppressParent,
                    IncludeType = isPruned ? LibraryIncludeFlags.None : dependency.IncludeType,
                };
            }

            return dependencies;

            static bool IsDependencyPruned(LibraryDependency dependency, IReadOnlyDictionary<string, PrunePackageReference> packagesToPrune)
            {
                if (packagesToPrune?.TryGetValue(dependency.Name, out PrunePackageReference packageToPrune) == true
                    && dependency.LibraryRange.VersionRange.Satisfies(packageToPrune.VersionRange.MaxVersion))
                {
                    return true;
                }
                return false;
            }
        }

        private List<ExternalProjectReference> GetChildReferences(ExternalProjectReference parent)
        {
            var children = new List<ExternalProjectReference>(parent.ExternalProjectReferences.Count);

            foreach (var reference in parent.ExternalProjectReferences)
            {
                ExternalProjectReference childReference;
                if (!_externalProjectsByUniqueName.TryGetValue(reference, out childReference))
                {
                    // Create a reference to mark that this project is unresolved here
                    childReference = new ExternalProjectReference(
                        uniqueName: reference,
                        packageSpec: null,
                        msbuildProjectPath: null,
                        projectReferences: Enumerable.Empty<string>());
                }

                children.Add(childReference);
            }

            return children;
        }
    }
}