File: Commands\Reference\FileBasedAppReferenceEditor.cs
Web Access
Project: 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 Microsoft.Build.Construction;
using Microsoft.Build.Execution;
using Microsoft.DotNet.Cli.Commands.Run;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.FileBasedPrograms;
using Microsoft.DotNet.ProjectTools;

namespace Microsoft.DotNet.Cli.Commands.Reference;

internal sealed class FileBasedAppReferenceEditor
{
    private readonly FileBasedAppSourceEditor _sourceEditor;
    private readonly ProjectInstance _projectInstance;

    public FileBasedAppReferenceEditor(string entryPointFilePath)
    {
        EntryPointFilePath = Path.GetFullPath(entryPointFilePath);
        EntryPointFileDirectory = Path.GetDirectoryName(EntryPointFilePath)
            ?? throw new InvalidOperationException($"Source file path '{EntryPointFilePath}' does not have a containing directory.");
        _sourceEditor = FileBasedAppSourceEditor.Load(SourceFile.Load(EntryPointFilePath));
        _projectInstance = CreateExpansionProjectInstance(EntryPointFilePath);
    }

    public string EntryPointFilePath { get; }

    public string EntryPointFileDirectory { get; }

    public IEnumerable<string> GetReferencesForDisplay()
    {
        foreach (var directive in _sourceEditor.Directives)
        {
            if (directive is CSharpDirective.Project projectDirective)
            {
                yield return projectDirective.Name;
            }
            else if (directive is CSharpDirective.Ref refDirective)
            {
                yield return refDirective.Name;
            }
        }
    }

    public int AddProjectReferences(IEnumerable<(string ProjectFilePath, string DirectiveInclude)> references)
    {
        int numberOfAddedReferences = 0;

        foreach (var reference in references)
        {
            if (TryGetProjectReferenceDisplay(reference.ProjectFilePath, out var existingReferenceDisplay))
            {
                Reporter.Output.WriteLine(string.Format(
                    CliStrings.ProjectAlreadyHasAreference,
                    existingReferenceDisplay));
                continue;
            }

            numberOfAddedReferences++;
            _sourceEditor.Add(new CSharpDirective.Project(default, reference.DirectiveInclude));
            Reporter.Output.WriteLine(string.Format(CliStrings.ReferenceAddedToTheProject, reference.DirectiveInclude));
        }

        return numberOfAddedReferences;
    }

    public int AddFileBasedAppReferences(IEnumerable<(string FilePath, string DirectiveInclude)> references)
    {
        int numberOfAddedReferences = 0;

        foreach (var reference in references)
        {
            if (TryGetFileBasedAppReferenceDisplay(reference.FilePath, out var existingReferenceDisplay))
            {
                Reporter.Output.WriteLine(string.Format(
                    CliStrings.ProjectAlreadyHasAreference,
                    existingReferenceDisplay));
                continue;
            }

            numberOfAddedReferences++;
            _sourceEditor.Add(new CSharpDirective.Ref(default, reference.DirectiveInclude));
            Reporter.Output.WriteLine(string.Format(CliStrings.ReferenceAddedToTheProject, reference.DirectiveInclude));
        }

        return numberOfAddedReferences;
    }

    public int RemoveReferences(IEnumerable<string> references)
    {
        int totalNumberOfRemovedReferences = 0;

        foreach (var reference in references)
        {
            int numberOfRemovedReferences = RemoveFileBasedAppReferences(reference);

            if (numberOfRemovedReferences == 0)
            {
                numberOfRemovedReferences = RemoveProjectReferences(reference);
            }

            if (numberOfRemovedReferences == 0)
            {
                Reporter.Output.WriteLine(string.Format(
                    CliStrings.ProjectReferenceCouldNotBeFound,
                    reference));
            }

            totalNumberOfRemovedReferences += numberOfRemovedReferences;
        }

        return totalNumberOfRemovedReferences;
    }

    public void Save() => _sourceEditor.SourceFile.Save();

    private int RemoveProjectReferences(string reference)
    {
        var referencePaths = GetProjectReferenceArgumentFullPathAlternatives(reference).ToList();
        var matchingDirectives = _sourceEditor.Directives
            .OfType<CSharpDirective.Project>()
            .Where(d => referencePaths.Any(p => PathsEqual(GetProjectDirectiveFullPath(d), p)))
            .OrderByDescending(d => d.Info.Span.Start)
            .ToList();

        foreach (var directive in matchingDirectives)
        {
            _sourceEditor.Remove(directive);
            Reporter.Output.WriteLine(string.Format(CliStrings.ProjectReferenceRemoved, directive.Name));
        }

        return matchingDirectives.Count;
    }

    private int RemoveFileBasedAppReferences(string reference)
    {
        var referencePath = Path.GetFullPath(reference);
        var matchingDirectives = _sourceEditor.Directives
            .OfType<CSharpDirective.Ref>()
            .Where(d => PathsEqual(GetRefDirectiveFullPath(d), referencePath))
            .OrderByDescending(d => d.Info.Span.Start)
            .ToList();

        foreach (var directive in matchingDirectives)
        {
            _sourceEditor.Remove(directive);
            Reporter.Output.WriteLine(string.Format(CliStrings.ProjectReferenceRemoved, directive.Name));
        }

        return matchingDirectives.Count;
    }

    private bool TryGetProjectReferenceDisplay(string projectFilePath, out string display)
    {
        var directive = _sourceEditor.Directives
            .OfType<CSharpDirective.Project>()
            .FirstOrDefault(d => PathsEqual(GetProjectDirectiveFullPath(d), projectFilePath));

        if (directive is null)
        {
            display = string.Empty;
            return false;
        }

        display = directive.Name;
        return true;
    }

    private bool TryGetFileBasedAppReferenceDisplay(string filePath, out string display)
    {
        var directive = _sourceEditor.Directives
            .OfType<CSharpDirective.Ref>()
            .FirstOrDefault(d => PathsEqual(GetRefDirectiveFullPath(d), filePath));

        if (directive is null)
        {
            display = string.Empty;
            return false;
        }

        display = directive.Name;
        return true;
    }

    private string GetProjectDirectiveFullPath(CSharpDirective.Project projectDirective)
    {
        var expandedDirective = projectDirective.WithName(
            _projectInstance.ExpandString(projectDirective.Name),
            CSharpDirective.Project.NameKind.Expanded);

        var resolvedDirective = expandedDirective.EnsureProjectFilePath(ErrorReporters.IgnoringReporter);
        return GetFullPathRelativeToEntryPointFileDirectory(resolvedDirective.Name);
    }

    private string GetRefDirectiveFullPath(CSharpDirective.Ref refDirective)
    {
        var expandedDirective = refDirective.WithName(
            _projectInstance.ExpandString(refDirective.Name),
            CSharpDirective.Ref.NameKind.Expanded);

        return expandedDirective.EnsureResolvedPath(ErrorReporters.IgnoringReporter).Name;
    }

    private IEnumerable<string> GetProjectReferenceArgumentFullPathAlternatives(string reference)
    {
        yield return ResolveProjectReferenceArgumentFullPath(Path.GetFullPath(reference));

        var appRelativePath = Path.GetFullPath(Path.Combine(EntryPointFileDirectory, reference.Replace('\\', '/')));
        if (!PathsEqual(appRelativePath, reference))
        {
            yield return ResolveProjectReferenceArgumentFullPath(appRelativePath);
        }
    }

    private static string ResolveProjectReferenceArgumentFullPath(string fullPath)
        => Directory.Exists(fullPath)
            ? MsbuildProject.GetProjectFileFromDirectory(fullPath)
            : fullPath;

    private string GetFullPathRelativeToEntryPointFileDirectory(string path)
        => Path.GetFullPath(Path.Combine(EntryPointFileDirectory, path.Replace('\\', '/')));

    private static bool PathsEqual(string left, string right)
        => string.Equals(
            NormalizePath(Path.GetFullPath(left)),
            NormalizePath(Path.GetFullPath(right)),
            StringComparison.OrdinalIgnoreCase);

    private static ProjectInstance CreateExpansionProjectInstance(string entryPointFilePath)
    {
        var projectRootElement = ProjectRootElement.Create();
        projectRootElement.FullPath = VirtualProjectBuilder.GetVirtualProjectPath(entryPointFilePath);
        return new ProjectInstance(projectRootElement);
    }

    private static string NormalizePath(string path) => path.Replace('\\', '/');
}