File: MSBuild\ProjectMap.cs
Web Access
Project: src\roslyn\src\Workspaces\MSBuild\Core\Microsoft.CodeAnalysis.Workspaces.MSBuild.csproj (Microsoft.CodeAnalysis.Workspaces.MSBuild)
// 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.Diagnostics.CodeAnalysis;
using System.Linq;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.MSBuild;

/// <summary>
/// A map of projects that can be optionally used with <see cref="MSBuildProjectLoader.LoadProjectInfoAsync"/> when loading a
/// project into a custom <see cref="Workspace"/>. To use, pass <see cref="Workspace.CurrentSolution"/> to <see cref="Create(Solution)"/>.
/// </summary>
public class ProjectMap
{
    /// <summary>
    /// A map of project path to <see cref="ProjectId"/>s. Note that there can be multiple <see cref="ProjectId"/>s per project path
    /// if the project is multi-targeted -- one for each target framework.
    /// </summary>
    private readonly Dictionary<string, HashSet<ProjectId>> _projectPathToProjectIdsMap;

    /// <summary>
    /// A map of project path to <see cref="ProjectInfo"/>s. Note that there can be multiple <see cref="ProjectId"/>s per project path
    /// if the project is multi-targeted -- one for each target framework.
    /// </summary>
    private readonly Dictionary<string, ImmutableArray<ProjectInfo>> _projectPathToProjectInfosMap;

    /// <summary>
    /// A map of <see cref="ProjectId"/> to the output file of the project (if any).
    /// </summary>
    private readonly Dictionary<ProjectId, string> _projectIdToOutputFilePathMap;

    /// <summary>
    /// A map of <see cref="ProjectId"/> to the output ref file of the project (if any).
    /// </summary>
    private readonly Dictionary<ProjectId, string> _projectIdToOutputRefFilePathMap;

    private ProjectMap()
    {
        _projectPathToProjectIdsMap = new Dictionary<string, HashSet<ProjectId>>(PathUtilities.Comparer);
        _projectPathToProjectInfosMap = new Dictionary<string, ImmutableArray<ProjectInfo>>(PathUtilities.Comparer);
        _projectIdToOutputFilePathMap = [];
        _projectIdToOutputRefFilePathMap = [];
    }

    /// <summary>
    /// Create an empty <see cref="ProjectMap"/>.
    /// </summary>
    public static ProjectMap Create() => new();

    /// <summary>
    /// Create a <see cref="ProjectMap"/> populated with the given <see cref="Solution"/>.
    /// </summary>
    /// <param name="solution">The <see cref="Solution"/> to populate the new <see cref="ProjectMap"/> with.</param>
    public static ProjectMap Create(Solution solution)
    {
        var projectMap = new ProjectMap();

        foreach (var project in solution.Projects)
        {
            projectMap.Add(project);
        }

        return projectMap;
    }

    /// <summary>
    /// Add a <see cref="Project"/> to this <see cref="ProjectMap"/>.
    /// </summary>
    /// <param name="project">The <see cref="Project"/> to add to this <see cref="ProjectMap"/>.</param>
    public void Add(Project project)
    {
        Add(project.Id, project.FilePath, project.OutputFilePath, project.OutputRefFilePath);
        AddProjectInfo(project.State.ProjectInfo);
    }

    private void Add(ProjectId projectId, string? projectPath, string? outputFilePath, string? outputRefFilePath)
    {
        if (!RoslynString.IsNullOrEmpty(projectPath))
        {
            _projectPathToProjectIdsMap.MultiAdd(projectPath, projectId);
        }

        if (!RoslynString.IsNullOrEmpty(outputFilePath))
        {
            _projectIdToOutputFilePathMap.Add(projectId, outputFilePath);
        }

        if (!RoslynString.IsNullOrEmpty(outputRefFilePath))
        {
            _projectIdToOutputRefFilePathMap.Add(projectId, outputRefFilePath);
        }
    }

    internal void AddProjectInfo(ProjectInfo projectInfo)
    {
        var projectFilePath = projectInfo.FilePath;
        if (RoslynString.IsNullOrEmpty(projectFilePath))
        {
            throw new ArgumentException(WorkspaceMSBuildResources.Project_does_not_have_a_path);
        }

        if (!_projectPathToProjectInfosMap.TryGetValue(projectFilePath, out var projectInfos))
        {
            projectInfos = [];
        }

        if (projectInfos.Contains(pi => pi.Id == projectInfo.Id))
        {
            throw new ArgumentException(WorkspaceMSBuildResources.Project_already_added);
        }

        projectInfos = projectInfos.Add(projectInfo);

        _projectPathToProjectInfosMap[projectFilePath] = projectInfos;
    }

    private ProjectId CreateProjectId(string? projectPath, string? outputFilePath, string? outputRefFilePath)
    {
        var newProjectId = ProjectId.CreateNewId(debugName: projectPath);
        Add(newProjectId, projectPath, outputFilePath, outputRefFilePath);
        return newProjectId;
    }

    internal ProjectId GetOrCreateProjectId(string projectPath)
    {
        if (!_projectPathToProjectIdsMap.TryGetValue(projectPath, out var projectIds))
        {
            projectIds = [];
            _projectPathToProjectIdsMap.Add(projectPath, projectIds);
        }

        return projectIds.Count == 1
            ? projectIds.Single()
            : CreateProjectId(projectPath, outputFilePath: null, outputRefFilePath: null);
    }

    internal ProjectId GetOrCreateProjectId(ProjectFileInfo projectFileInfo)
    {
        var projectPath = projectFileInfo.FilePath;
        var outputFilePath = projectFileInfo.OutputFilePath;
        var outputRefFilePath = projectFileInfo.OutputRefFilePath;

        if (projectPath is not null && TryGetIdsByProjectPath(projectPath, out var projectIds))
        {
            if (TryFindOutputFileRefPathInProjectIdSet(outputRefFilePath, projectIds, out var projectId) ||
                TryFindOutputFilePathInProjectIdSet(outputFilePath, projectIds, out projectId))
            {
                return projectId;
            }
        }

        return CreateProjectId(projectPath, outputFilePath, outputRefFilePath);
    }

    private bool TryFindOutputFileRefPathInProjectIdSet(string? outputRefFilePath, HashSet<ProjectId> set, [NotNullWhen(true)] out ProjectId? result)
        => TryFindPathInProjectIdSet(outputRefFilePath, GetOutputRefFilePathById, set, out result);

    private bool TryFindOutputFilePathInProjectIdSet(string? outputFilePath, HashSet<ProjectId> set, [NotNullWhen(true)] out ProjectId? result)
        => TryFindPathInProjectIdSet(outputFilePath, GetOutputFilePathById, set, out result);

    private static bool TryFindPathInProjectIdSet(string? path, Func<ProjectId, string?> getPathById, HashSet<ProjectId> set, [NotNullWhen(true)] out ProjectId? result)
    {
        if (!RoslynString.IsNullOrEmpty(path))
        {
            foreach (var id in set)
            {
                var p = getPathById(id);

                if (PathUtilities.Comparer.Equals(p, path))
                {
                    result = id;
                    return true;
                }
            }
        }

        result = null;
        return false;
    }

    internal string? GetOutputRefFilePathById(ProjectId projectId)
        => TryGetOutputRefFilePathById(projectId, out var path)
            ? path
            : null;

    internal string? GetOutputFilePathById(ProjectId projectId)
        => TryGetOutputFilePathById(projectId, out var path)
            ? path
            : null;

    internal bool TryGetIdsByProjectPath(string projectPath, [NotNullWhen(true)] out HashSet<ProjectId>? ids)
        => _projectPathToProjectIdsMap.TryGetValue(projectPath, out ids);

    internal bool TryGetOutputFilePathById(ProjectId id, [NotNullWhen(true)] out string? outputFilePath)
        => _projectIdToOutputFilePathMap.TryGetValue(id, out outputFilePath);

    internal bool TryGetOutputRefFilePathById(ProjectId id, [NotNullWhen(true)] out string? outputRefFilePath)
        => _projectIdToOutputRefFilePathMap.TryGetValue(id, out outputRefFilePath);

    internal bool TryGetProjectInfosByProjectPath(string projectPath, out ImmutableArray<ProjectInfo> projectInfos)
        => _projectPathToProjectInfosMap.TryGetValue(projectPath, out projectInfos);
}