File: MSBuild\ProjectMap.cs
Web Access
Project: src\src\Workspaces\Core\MSBuild\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>
        /// <returns></returns>
        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);
    }
}