File: Commands\Package\VirtualProjectPackageReflector.cs
Web Access
Project: src\src\sdk\src\Cli\dotnet\dotnet.csproj (dotnet)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using Microsoft.Build.Construction;
using Microsoft.DotNet.Cli.Commands.Run;
using Microsoft.DotNet.FileBasedPrograms;

namespace Microsoft.DotNet.Cli.Commands.Package;

/// <summary>
/// Utility for reflecting changes in a <see cref="ProjectRootElement"/> (modified by NuGet)
/// back to <c>#:package</c> directives in a C# source file.
/// </summary>
internal static class VirtualProjectPackageReflector
{
    /// <summary>
    /// Reads <c>PackageReference</c> items from the given <paramref name="projectRootElement"/> and
    /// updates the <c>#:package</c> directives in the source file at <paramref name="entryPointFilePath"/> to match.
    /// </summary>
    internal static void ReflectChangesToDirectives(ProjectRootElement projectRootElement, string entryPointFilePath)
    {
        // Collect PackageReference items from the modified virtual project.
        var packageReferences = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
        foreach (var itemGroup in projectRootElement.ItemGroups)
        {
            foreach (var item in itemGroup.Items)
            {
                if (string.Equals(item.ItemType, "PackageReference", StringComparison.OrdinalIgnoreCase))
                {
                    Debug.Assert(!string.IsNullOrEmpty(item.Include), "Expected only PackageReference Include items in virtual project.");
                    var version = item.Metadata.FirstOrDefault(
                        m => string.Equals(m.Name, "Version", StringComparison.OrdinalIgnoreCase))?.Value;
                    packageReferences[item.Include] = version;
                }
            }
        }

        // Load the source file and its current directives.
        var editor = FileBasedAppSourceEditor.Load(SourceFile.Load(entryPointFilePath));
        var directives = editor.Directives;

        // Remove directives for packages that are no longer in the project.
        // Process in reverse order to avoid invalidating spans.
        for (int i = directives.Length - 1; i >= 0; i--)
        {
            if (directives[i] is CSharpDirective.Package p &&
                !packageReferences.ContainsKey(p.Name))
            {
                editor.Remove(directives[i]);
            }
        }

        // Add or update directives for packages in the project.
        foreach (var (name, version) in packageReferences)
        {
            // Always update existing directives (version might have changed) and add new ones.
            editor.Add(new CSharpDirective.Package(default) { Name = name, Version = version });
        }

        editor.SourceFile.Save();
    }
}