File: Workspace\ProjectSystem\ProjectSystemProjectFactory.ProjectUpdateState.cs
Web Access
Project: src\src\Workspaces\Core\Portable\Microsoft.CodeAnalysis.Workspaces.csproj (Microsoft.CodeAnalysis.Workspaces)
// 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 Microsoft.CodeAnalysis.Diagnostics;
 
namespace Microsoft.CodeAnalysis.Workspaces.ProjectSystem;
 
internal sealed partial class ProjectSystemProjectFactory
{
    /// <summary>
    /// Immutable data type that holds the current state of the project system factory as well as storing any
    /// incremental state changes in the current workspace update.
    /// 
    /// This state is updated by various project system update operations under the <see cref="_gate"/>. Importantly,
    /// this immutable type allows us to discard updates to the state that fail to apply due to interceding workspace
    /// operations.
    /// 
    /// There are two kinds of state that this type holds that need to support discarding:
    /// <list type="number">
    /// <item>Global state for the <see cref="ProjectSystemProjectFactory"/> (various maps of project information). This
    /// state must be saved between different changes.</item>
    /// <item>Incremental state for the current change being processed.  This state has information that is cannot be
    /// resilient to being applied multiple times during the workspace update, so is saved to be applied only once the
    /// workspace update is successful.</item>
    /// </list>
    /// </summary>
    /// <param name="ProjectsByOutputPath">
    /// Global state representing a multimap from an output path to the project outputting to it. Ideally, this
    /// shouldn't ever actually be a true multimap, since we shouldn't have two projects outputting to the same path,
    /// but any bug by a project adding the wrong output path means we could end up with some duplication. In that case,
    /// we'll temporarily have two until (hopefully) somebody removes it.
    /// </param>
    /// <param name="ProjectReferenceInfos">
    /// Global state containing output paths and converted project reference information for each project.
    /// </param>
    /// <param name="RemovedMetadataReferences">
    /// Incremental state containing metadata references removed in the current update.
    /// </param>
    /// <param name="AddedMetadataReferences">
    /// Incremental state containing metadata references added in the current update.
    /// </param>
    /// <param name="RemovedAnalyzerReferences">
    /// Incremental state containing analyzer references removed in the current update.
    /// </param>
    /// <param name="AddedAnalyzerReferences">
    /// Incremental state containing analyzer references added in the current update.
    /// </param>
    public sealed record class ProjectUpdateState(
        ImmutableDictionary<string, ImmutableArray<ProjectId>> ProjectsByOutputPath,
        ImmutableDictionary<ProjectId, ProjectReferenceInformation> ProjectReferenceInfos,
        ImmutableArray<PortableExecutableReference> RemovedMetadataReferences,
        ImmutableArray<PortableExecutableReference> AddedMetadataReferences,
        ImmutableArray<string> RemovedAnalyzerReferences,
        ImmutableArray<string> AddedAnalyzerReferences)
    {
        public static ProjectUpdateState Empty = new(
            ImmutableDictionary<string, ImmutableArray<ProjectId>>.Empty.WithComparers(StringComparer.OrdinalIgnoreCase),
            ImmutableDictionary<ProjectId, ProjectReferenceInformation>.Empty, [], [], [], []);
 
        public ProjectUpdateState WithProjectReferenceInfo(ProjectId projectId, ProjectReferenceInformation projectReferenceInformation)
        {
            return this with
            {
                ProjectReferenceInfos = ProjectReferenceInfos.SetItem(projectId, projectReferenceInformation)
            };
        }
 
        public ProjectUpdateState WithProjectOutputPath(string projectOutputPath, ProjectId projectId)
        {
            return this with
            {
                ProjectsByOutputPath = AddProject(projectOutputPath, projectId, ProjectsByOutputPath)
            };
 
            static ImmutableDictionary<string, ImmutableArray<ProjectId>> AddProject(string path, ProjectId projectId, ImmutableDictionary<string, ImmutableArray<ProjectId>> map)
            {
                if (!map.TryGetValue(path, out var projects))
                {
                    return map.Add(path, [projectId]);
                }
                else
                {
                    return map.SetItem(path, projects.Add(projectId));
                }
            }
        }
 
        public ProjectUpdateState RemoveProjectOutputPath(string projectOutputPath, ProjectId projectId)
        {
            return this with
            {
                ProjectsByOutputPath = RemoveProject(projectOutputPath, projectId, ProjectsByOutputPath)
            };
 
            static ImmutableDictionary<string, ImmutableArray<ProjectId>> RemoveProject(string path, ProjectId projectId, ImmutableDictionary<string, ImmutableArray<ProjectId>> map)
            {
                if (map.TryGetValue(path, out var projects))
                {
                    projects = projects.Remove(projectId);
                    if (projects.IsEmpty)
                    {
                        return map.Remove(path);
                    }
                    else
                    {
                        return map.SetItem(path, projects);
                    }
                }
 
                return map;
            }
        }
 
        public ProjectUpdateState WithIncrementalMetadataReferenceRemoved(PortableExecutableReference reference)
            => this with { RemovedMetadataReferences = RemovedMetadataReferences.Add(reference) };
 
        public ProjectUpdateState WithIncrementalMetadataReferenceAdded(PortableExecutableReference reference)
            => this with { AddedMetadataReferences = AddedMetadataReferences.Add(reference) };
 
        public ProjectUpdateState WithIncrementalAnalyzerReferenceRemoved(string reference)
            => this with { RemovedAnalyzerReferences = RemovedAnalyzerReferences.Add(reference) };
 
        public ProjectUpdateState WithIncrementalAnalyzerReferencesRemoved(List<string> references)
            => this with { RemovedAnalyzerReferences = RemovedAnalyzerReferences.AddRange(references) };
 
        public ProjectUpdateState WithIncrementalAnalyzerReferenceAdded(string reference)
            => this with { AddedAnalyzerReferences = AddedAnalyzerReferences.Add(reference) };
 
        public ProjectUpdateState WithIncrementalAnalyzerReferencesAdded(List<string> references)
            => this with { AddedAnalyzerReferences = AddedAnalyzerReferences.AddRange(references) };
 
        /// <summary>
        /// Returns a new instance with any incremental state that should not be saved between updates cleared.
        /// </summary>
        public ProjectUpdateState ClearIncrementalState()
            => this with
            {
                RemovedMetadataReferences = [],
                AddedMetadataReferences = [],
                RemovedAnalyzerReferences = [],
                AddedAnalyzerReferences = [],
            };
    }
 
    public record struct ProjectReferenceInformation(ImmutableArray<string> OutputPaths, ImmutableArray<(string path, ProjectReference ProjectReference)> ConvertedProjectReferences)
    {
        internal ProjectReferenceInformation WithConvertedProjectReference(string path, ProjectReference projectReference)
        {
            return this with
            {
                ConvertedProjectReferences = ConvertedProjectReferences.Add((path, projectReference))
            };
        }
    }
}