File: MSBuild\ProjectFile\ProjectFile.cs
Web Access
Project: src\src\Workspaces\MSBuild\BuildHost\Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.csproj (Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Roslyn.Utilities;
using MSB = Microsoft.Build;
 
namespace Microsoft.CodeAnalysis.MSBuild;
 
internal sealed class ProjectFile(
    string language,
    MSB.Evaluation.Project? project,
    ProjectCommandLineProvider commandLineProvider,
    ProjectBuildManager buildManager,
    DiagnosticLog log) : IProjectFile
{
    public string FilePath
        => project?.FullPath ?? string.Empty;
 
    public ImmutableArray<DiagnosticLogItem> GetDiagnosticLogItems()
        => [.. log];
 
    /// <summary>
    /// Gets project file information asynchronously. Note that this can produce multiple
    /// instances of <see cref="ProjectFileInfo"/> if the project is multi-targeted: one for
    /// each target framework.
    /// </summary>
    public async Task<ImmutableArray<ProjectFileInfo>> GetProjectFileInfosAsync(CancellationToken cancellationToken)
    {
        if (project is null)
        {
            return [ProjectFileInfo.CreateEmpty(language, filePath: null)];
        }
 
        var projectInstances = await buildManager.BuildProjectInstancesAsync(project, log, cancellationToken).ConfigureAwait(false);
 
        return projectInstances.SelectAsArray(
            instance => new ProjectInstanceReader(commandLineProvider, instance, project).CreateProjectFileInfo());
    }
 
    public void AddDocument(string filePath, string? logicalPath = null)
    {
        if (project is null)
        {
            return;
        }
 
        var relativePath = PathUtilities.GetRelativePath(project.DirectoryPath, filePath);
 
        Dictionary<string, string>? metadata = null;
        if (logicalPath != null && relativePath != logicalPath)
        {
            metadata = new Dictionary<string, string>
            {
                { MetadataNames.Link, logicalPath }
            };
 
            relativePath = filePath; // link to full path
        }
 
        project.AddItem(ItemNames.Compile, relativePath, metadata);
    }
 
    public void RemoveDocument(string filePath)
    {
        if (project is null)
        {
            return;
        }
 
        var relativePath = PathUtilities.GetRelativePath(project.DirectoryPath, filePath);
 
        var items = project.GetItems(ItemNames.Compile);
        var item = items.FirstOrDefault(it => PathUtilities.PathsEqual(it.EvaluatedInclude, relativePath)
                                           || PathUtilities.PathsEqual(it.EvaluatedInclude, filePath));
        if (item != null)
        {
            project.RemoveItem(item);
        }
    }
 
    public void AddMetadataReference(string metadataReferenceIdentity, ImmutableArray<string> aliases, string? hintPath)
    {
        if (project is null)
        {
            return;
        }
 
        var metadata = new Dictionary<string, string>();
        if (!aliases.IsEmpty)
            metadata.Add(MetadataNames.Aliases, string.Join(",", aliases));
 
        if (hintPath is not null)
            metadata.Add(MetadataNames.HintPath, hintPath);
 
        project.AddItem(ItemNames.Reference, metadataReferenceIdentity, metadata);
    }
 
    public void RemoveMetadataReference(string shortAssemblyName, string fullAssemblyName, string filePath)
    {
        if (project is null)
        {
            return;
        }
 
        var item = FindReferenceItem(shortAssemblyName, fullAssemblyName, filePath);
        if (item != null)
        {
            project.RemoveItem(item);
        }
    }
 
    private MSB.Evaluation.ProjectItem FindReferenceItem(string shortAssemblyName, string fullAssemblyName, string filePath)
    {
        Contract.ThrowIfNull(project, "The project was not loaded.");
 
        var references = project.GetItems(ItemNames.Reference);
        MSB.Evaluation.ProjectItem? item = null;
 
        var fileName = Path.GetFileNameWithoutExtension(filePath);
 
        // check for short name match
        item = references.FirstOrDefault(it => string.Compare(it.EvaluatedInclude, shortAssemblyName, StringComparison.OrdinalIgnoreCase) == 0);
        if (item is not null)
            return item;
 
        // check for full name match
        item = references.FirstOrDefault(it => string.Compare(it.EvaluatedInclude, fullAssemblyName, StringComparison.OrdinalIgnoreCase) == 0);
        if (item is not null)
            return item;
 
        // check for file path match
        var relativePath = PathUtilities.GetRelativePath(project.DirectoryPath, filePath);
 
        item = references.FirstOrDefault(it => PathUtilities.PathsEqual(it.EvaluatedInclude, filePath)
                                                || PathUtilities.PathsEqual(it.EvaluatedInclude, relativePath)
                                                || PathUtilities.PathsEqual(GetHintPath(it), filePath)
                                                || PathUtilities.PathsEqual(GetHintPath(it), relativePath));
 
        if (item is not null)
            return item;
 
        var partialName = shortAssemblyName + ",";
        var items = references.Where(it => it.EvaluatedInclude.StartsWith(partialName, StringComparison.OrdinalIgnoreCase)).ToList();
        if (items.Count == 1)
        {
            return items[0];
        }
 
        throw new InvalidOperationException($"Unable to find reference item '{shortAssemblyName}'");
    }
 
    private static string GetHintPath(MSB.Evaluation.ProjectItem item)
        => item.Metadata.FirstOrDefault(m => string.Equals(m.Name, MetadataNames.HintPath, StringComparison.OrdinalIgnoreCase))?.EvaluatedValue ?? string.Empty;
 
    public void AddProjectReference(string projectName, ProjectFileReference reference)
    {
        if (project is null)
        {
            return;
        }
 
        var metadata = new Dictionary<string, string>
        {
            { MetadataNames.Name, projectName }
        };
 
        if (!reference.Aliases.IsEmpty)
        {
            metadata.Add(MetadataNames.Aliases, string.Join(",", reference.Aliases));
        }
 
        var relativePath = PathUtilities.GetRelativePath(project.DirectoryPath, reference.Path);
        project.AddItem(ItemNames.ProjectReference, relativePath, metadata);
    }
 
    public void RemoveProjectReference(string projectName, string projectFilePath)
    {
        if (project is null)
        {
            return;
        }
 
        var item = FindProjectReferenceItem(projectName, projectFilePath);
        if (item != null)
        {
            project.RemoveItem(item);
        }
    }
 
    private MSB.Evaluation.ProjectItem? FindProjectReferenceItem(string projectName, string projectFilePath)
    {
        if (project is null)
        {
            return null;
        }
 
        var references = project.GetItems(ItemNames.ProjectReference);
        var relativePath = PathUtilities.GetRelativePath(project.DirectoryPath, projectFilePath);
 
        MSB.Evaluation.ProjectItem? item = null;
 
        // find by project file path
        item = references.First(it => PathUtilities.PathsEqual(it.EvaluatedInclude, relativePath)
                                   || PathUtilities.PathsEqual(it.EvaluatedInclude, projectFilePath));
 
        // try to find by project name
        item ??= references.First(it => string.Compare(projectName, it.GetMetadataValue(MetadataNames.Name), StringComparison.OrdinalIgnoreCase) == 0);
 
        return item;
    }
 
    public void AddAnalyzerReference(string fullPath)
    {
        if (project is null)
        {
            return;
        }
 
        var relativePath = PathUtilities.GetRelativePath(project.DirectoryPath, fullPath);
        project.AddItem(ItemNames.Analyzer, relativePath);
    }
 
    public void RemoveAnalyzerReference(string fullPath)
    {
        if (project is null)
        {
            return;
        }
 
        var relativePath = PathUtilities.GetRelativePath(project.DirectoryPath, fullPath);
 
        var analyzers = project.GetItems(ItemNames.Analyzer);
        var item = analyzers.FirstOrDefault(it => PathUtilities.PathsEqual(it.EvaluatedInclude, relativePath)
                                                || PathUtilities.PathsEqual(it.EvaluatedInclude, fullPath));
        if (item != null)
        {
            project.RemoveItem(item);
        }
    }
 
    public void Save()
    {
        if (project is null)
        {
            return;
        }
 
        project.Save();
    }
}