File: Workspace\Solution\SolutionState.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 System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Collections;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis;
 
internal readonly record struct StateChange(
    SolutionState NewSolutionState,
    ProjectState OldProjectState,
    ProjectState NewProjectState);
 
/// <summary>
/// Represents a set of projects and their source code documents.
///
/// this is a green node of Solution like ProjectState/DocumentState are for
/// Project and Document.
/// </summary>
internal sealed partial class SolutionState
{
    public static readonly IEqualityComparer<string> FilePathComparer = CachingFilePathComparer.Instance;
 
    // the version of the workspace this solution is from
    public int WorkspaceVersion { get; }
    public string? WorkspaceKind { get; }
    public SolutionServices Services { get; }
    public SolutionOptionSet Options { get; }
    public IReadOnlyList<AnalyzerReference> AnalyzerReferences { get; }
 
    /// <summary>
    /// Fallback analyzer config options by language. The set of languages does not need to match the set of langauges of projects included in the surrent solution snapshot.
    /// </summary>
    public ImmutableDictionary<string, StructuredAnalyzerConfigOptions> FallbackAnalyzerOptions { get; } = ImmutableDictionary<string, StructuredAnalyzerConfigOptions>.Empty;
 
    /// <summary>
    /// Number of projects in the solution of the given language.  The value is guaranteed to always be greater than zero.
    /// If the project count does ever hit zero then there simply is no key/value pair for that language in this map.
    /// </summary>
    internal ImmutableDictionary<string, int> ProjectCountByLanguage { get; } = ImmutableDictionary<string, int>.Empty;
 
    private readonly ProjectDependencyGraph _dependencyGraph;
 
    // holds on data calculated based on the AnalyzerReferences list
    private readonly Lazy<HostDiagnosticAnalyzers> _lazyAnalyzers;
 
    private ImmutableDictionary<string, ImmutableArray<DocumentId>> _lazyFilePathToRelatedDocumentIds = ImmutableDictionary<string, ImmutableArray<DocumentId>>.Empty.WithComparers(FilePathComparer);
 
    private SolutionState(
        string? workspaceKind,
        int workspaceVersion,
        SolutionServices services,
        SolutionInfo.SolutionAttributes solutionAttributes,
        IReadOnlyList<ProjectId> projectIds,
        SolutionOptionSet options,
        IReadOnlyList<AnalyzerReference> analyzerReferences,
        ImmutableDictionary<string, StructuredAnalyzerConfigOptions> fallbackAnalyzerOptions,
        ImmutableDictionary<string, int> projectCountByLanguage,
        ImmutableDictionary<ProjectId, ProjectState> idToProjectStateMap,
        ProjectDependencyGraph dependencyGraph,
        Lazy<HostDiagnosticAnalyzers>? lazyAnalyzers)
    {
        WorkspaceKind = workspaceKind;
        WorkspaceVersion = workspaceVersion;
        SolutionAttributes = solutionAttributes;
        Services = services;
        ProjectIds = projectIds;
        Options = options;
        AnalyzerReferences = analyzerReferences;
        FallbackAnalyzerOptions = fallbackAnalyzerOptions;
        ProjectCountByLanguage = projectCountByLanguage;
        ProjectStates = idToProjectStateMap;
        _dependencyGraph = dependencyGraph;
        _lazyAnalyzers = lazyAnalyzers ?? CreateLazyHostDiagnosticAnalyzers(analyzerReferences);
 
        // when solution state is changed, we recalculate its checksum
        _lazyChecksums = AsyncLazy.Create(static (self, c) =>
            self.ComputeChecksumsAsync(projectConeId: null, c),
            arg: this);
 
        CheckInvariants();
 
        // make sure we don't accidentally capture any state but the list of references:
        static Lazy<HostDiagnosticAnalyzers> CreateLazyHostDiagnosticAnalyzers(IReadOnlyList<AnalyzerReference> analyzerReferences)
            => new(() => new HostDiagnosticAnalyzers(analyzerReferences));
    }
 
    public SolutionState(
        string? workspaceKind,
        SolutionServices services,
        SolutionInfo.SolutionAttributes solutionAttributes,
        SolutionOptionSet options,
        IReadOnlyList<AnalyzerReference> analyzerReferences,
        ImmutableDictionary<string, StructuredAnalyzerConfigOptions> fallbackAnalyzerOptions)
        : this(
            workspaceKind,
            workspaceVersion: 0,
            services,
            solutionAttributes,
            projectIds: SpecializedCollections.EmptyBoxedImmutableArray<ProjectId>(),
            options,
            analyzerReferences,
            fallbackAnalyzerOptions,
            projectCountByLanguage: ImmutableDictionary<string, int>.Empty,
            idToProjectStateMap: ImmutableDictionary<ProjectId, ProjectState>.Empty,
            dependencyGraph: ProjectDependencyGraph.Empty,
            lazyAnalyzers: null)
    {
    }
 
    public HostDiagnosticAnalyzers Analyzers => _lazyAnalyzers.Value;
 
    public SolutionInfo.SolutionAttributes SolutionAttributes { get; }
 
    public ImmutableDictionary<ProjectId, ProjectState> ProjectStates { get; }
 
    /// <summary>
    /// The Id of the solution. Multiple solution instances may share the same Id.
    /// </summary>
    public SolutionId Id => SolutionAttributes.Id;
 
    /// <summary>
    /// The path to the solution file or null if there is no solution file.
    /// </summary>
    public string? FilePath => SolutionAttributes.FilePath;
 
    /// <summary>
    /// The solution version. This equates to the solution file's version.
    /// </summary>
    public VersionStamp Version => SolutionAttributes.Version;
 
    /// <summary>
    /// A list of all the ids for all the projects contained by the solution.
    /// </summary>
    public IReadOnlyList<ProjectId> ProjectIds { get; }
 
    private void CheckInvariants()
    {
        // Run these quick checks all the time.  We need to know immediately if we violate these.
        Contract.ThrowIfFalse(ProjectStates.Count == ProjectIds.Count);
        Contract.ThrowIfFalse(ProjectStates.Count == _dependencyGraph.ProjectIds.Count);
 
        // Only run this in debug builds; even the .SetEquals() call across all projects can be expensive when there's a lot of them.
#if DEBUG
        // project ids must be the same:
        Debug.Assert(ProjectStates.Keys.SetEquals(ProjectIds));
        Debug.Assert(ProjectStates.Keys.SetEquals(_dependencyGraph.ProjectIds));
#endif
    }
 
    internal SolutionState Branch(
        ImmutableDictionary<string, int>? projectCountByLanguage = null,
        SolutionInfo.SolutionAttributes? solutionAttributes = null,
        IReadOnlyList<ProjectId>? projectIds = null,
        SolutionOptionSet? options = null,
        IReadOnlyList<AnalyzerReference>? analyzerReferences = null,
        ImmutableDictionary<string, StructuredAnalyzerConfigOptions>? fallbackAnalyzerOptions = null,
        ImmutableDictionary<ProjectId, ProjectState>? idToProjectStateMap = null,
        ProjectDependencyGraph? dependencyGraph = null)
    {
        solutionAttributes ??= SolutionAttributes;
        projectIds ??= ProjectIds;
        idToProjectStateMap ??= ProjectStates;
        options ??= Options;
        analyzerReferences ??= AnalyzerReferences;
        fallbackAnalyzerOptions ??= FallbackAnalyzerOptions;
        projectCountByLanguage ??= ProjectCountByLanguage;
        dependencyGraph ??= _dependencyGraph;
 
        var analyzerReferencesEqual = AnalyzerReferences.SequenceEqual(analyzerReferences);
 
        if (solutionAttributes == SolutionAttributes &&
            projectIds == ProjectIds &&
            options == Options &&
            analyzerReferencesEqual &&
            fallbackAnalyzerOptions == FallbackAnalyzerOptions &&
            projectCountByLanguage == ProjectCountByLanguage &&
            idToProjectStateMap == ProjectStates &&
            dependencyGraph == _dependencyGraph)
        {
            return this;
        }
 
        return new SolutionState(
            WorkspaceKind,
            WorkspaceVersion,
            Services,
            solutionAttributes,
            projectIds,
            options,
            analyzerReferences,
            fallbackAnalyzerOptions,
            projectCountByLanguage,
            idToProjectStateMap,
            dependencyGraph,
            analyzerReferencesEqual ? _lazyAnalyzers : null);
    }
 
    /// <summary>
    /// Updates the solution with specified workspace kind, workspace version and services.
    /// This implicitly also changes the value of <see cref="Solution.Workspace"/> for this solution,
    /// since that is extracted from <see cref="SolutionServices"/> for backwards compatibility.
    /// </summary>
    public SolutionState WithNewWorkspace(
        string? workspaceKind,
        int workspaceVersion,
        SolutionServices services)
    {
        if (workspaceKind == WorkspaceKind &&
            workspaceVersion == WorkspaceVersion &&
            services == Services)
        {
            return this;
        }
 
        // Note: this will potentially have problems if the workspace services are different, as some services
        // get locked-in by document states and project states when first constructed.
        return new SolutionState(
            workspaceKind,
            workspaceVersion,
            services,
            SolutionAttributes,
            ProjectIds,
            Options,
            AnalyzerReferences,
            FallbackAnalyzerOptions,
            ProjectCountByLanguage,
            ProjectStates,
            _dependencyGraph,
            _lazyAnalyzers);
    }
 
    /// <summary>
    /// The version of the most recently modified project.
    /// </summary>
    public VersionStamp GetLatestProjectVersion()
    {
        // this may produce a version that is out of sync with the actual Document versions.
        var latestVersion = VersionStamp.Default;
        foreach (var project in this.ProjectStates.Values)
        {
            latestVersion = project.Version.GetNewerVersion(latestVersion);
        }
 
        return latestVersion;
    }
 
    /// <summary>
    /// True if the solution contains a project with the specified project ID.
    /// </summary>
    public bool ContainsProject([NotNullWhen(returnValue: true)] ProjectId? projectId)
        => projectId != null && ProjectStates.ContainsKey(projectId);
 
    /// <summary>
    /// True if the solution contains the document in one of its projects
    /// </summary>
    public bool ContainsDocument([NotNullWhen(returnValue: true)] DocumentId? documentId)
    {
        return
            documentId != null &&
            this.ContainsProject(documentId.ProjectId) &&
            this.GetProjectState(documentId.ProjectId)!.DocumentStates.Contains(documentId);
    }
 
    /// <summary>
    /// True if the solution contains the additional document in one of its projects
    /// </summary>
    public bool ContainsAdditionalDocument([NotNullWhen(returnValue: true)] DocumentId? documentId)
    {
        return
            documentId != null &&
            this.ContainsProject(documentId.ProjectId) &&
            this.GetProjectState(documentId.ProjectId)!.AdditionalDocumentStates.Contains(documentId);
    }
 
    /// <summary>
    /// True if the solution contains the analyzer config document in one of its projects
    /// </summary>
    public bool ContainsAnalyzerConfigDocument([NotNullWhen(returnValue: true)] DocumentId? documentId)
    {
        return
            documentId != null &&
            this.ContainsProject(documentId.ProjectId) &&
            this.GetProjectState(documentId.ProjectId)!.AnalyzerConfigDocumentStates.Contains(documentId);
    }
 
    internal DocumentState GetRequiredDocumentState(DocumentId documentId)
        => GetRequiredProjectState(documentId.ProjectId).DocumentStates.GetRequiredState(documentId);
 
    private AdditionalDocumentState GetRequiredAdditionalDocumentState(DocumentId documentId)
        => GetRequiredProjectState(documentId.ProjectId).AdditionalDocumentStates.GetRequiredState(documentId);
 
    private AnalyzerConfigDocumentState GetRequiredAnalyzerConfigDocumentState(DocumentId documentId)
        => GetRequiredProjectState(documentId.ProjectId).AnalyzerConfigDocumentStates.GetRequiredState(documentId);
 
    public ProjectState? GetProjectState(ProjectId projectId)
        => ProjectStates.TryGetValue(projectId, out var state) ? state : null;
 
    public ProjectState GetRequiredProjectState(ProjectId projectId)
    {
        var result = GetProjectState(projectId);
        Contract.ThrowIfNull(result);
        return result;
    }
 
    /// <summary>
    /// Create a new solution instance that includes projects with the specified project information.
    /// </summary>
    public SolutionState AddProjects(ArrayBuilder<ProjectInfo> projectInfos)
    {
        Contract.ThrowIfTrue(projectInfos.HasDuplicates(static p => p.Id), "Duplicate ProjectId provided");
 
        if (projectInfos.Count == 0)
            return this;
 
        var langaugeCountDeltas = new TemporaryArray<(string language, int count)>();
 
        using var _ = ArrayBuilder<ProjectState>.GetInstance(projectInfos.Count, out var projectStates);
        foreach (var projectInfo in projectInfos)
            projectStates.Add(CreateProjectState(projectInfo));
 
        return AddProjects(projectStates);
 
        ProjectState CreateProjectState(ProjectInfo projectInfo)
        {
            if (projectInfo == null)
                throw new ArgumentNullException(nameof(projectInfo));
 
            var projectId = projectInfo.Id;
 
            var language = projectInfo.Language;
            if (language == null)
                throw new ArgumentNullException(nameof(language));
 
            var displayName = projectInfo.Name;
            if (displayName == null)
                throw new ArgumentNullException(nameof(displayName));
 
            CheckNotContainsProject(projectId);
 
            var languageServices = Services.GetLanguageServices(language);
            if (languageServices == null)
                throw new ArgumentException(string.Format(WorkspacesResources.The_language_0_is_not_supported, language));
 
            if (!FallbackAnalyzerOptions.TryGetValue(language, out var fallbackAnalyzerOptions))
            {
                fallbackAnalyzerOptions = StructuredAnalyzerConfigOptions.Empty;
            }
 
            AddLanguageCountDelta(ref langaugeCountDeltas, language, amount: +1);
 
            var newProject = new ProjectState(languageServices, projectInfo, fallbackAnalyzerOptions);
            return newProject;
        }
 
        SolutionState AddProjects(ArrayBuilder<ProjectState> projectStates)
        {
            // changed project list so, increment version.
            var newSolutionAttributes = SolutionAttributes.With(version: Version.GetNewerVersion());
 
            using var _1 = ArrayBuilder<ProjectId>.GetInstance(ProjectIds.Count + projectStates.Count, out var newProjectIdsBuilder);
            using var _2 = PooledHashSet<ProjectId>.GetInstance(out var addedProjectIds);
            var newStateMapBuilder = ProjectStates.ToBuilder();
 
            newProjectIdsBuilder.AddRange(ProjectIds);
 
            foreach (var projectState in projectStates)
            {
                addedProjectIds.Add(projectState.Id);
                newProjectIdsBuilder.Add(projectState.Id);
                newStateMapBuilder.Add(projectState.Id, projectState);
            }
 
            var newProjectIds = newProjectIdsBuilder.ToBoxedImmutableArray();
            var newStateMap = newStateMapBuilder.ToImmutable();
 
            // TODO: it would be nice to update these graphs without so much forking.
            var newDependencyGraph = _dependencyGraph;
            foreach (var projectState in projectStates)
            {
                var projectId = projectState.Id;
                newDependencyGraph = newDependencyGraph
                    .WithAdditionalProject(projectId)
                    .WithAdditionalProjectReferences(projectId, projectState.ProjectReferences);
            }
 
            // It's possible that another project already in newStateMap has a reference to this project that we're adding,
            // since we allow dangling references like that. If so, we'll need to link those in too.
            foreach (var (projectId, newState) in newStateMap)
            {
                foreach (var projectReference in newState.ProjectReferences)
                {
                    if (addedProjectIds.Contains(projectReference.ProjectId))
                        newDependencyGraph = newDependencyGraph.WithAdditionalProjectReferences(projectId, [projectReference]);
                }
            }
 
            return Branch(
                solutionAttributes: newSolutionAttributes,
                projectIds: newProjectIds,
                idToProjectStateMap: newStateMap,
                projectCountByLanguage: AddLanguageCounts(ProjectCountByLanguage, langaugeCountDeltas),
                dependencyGraph: newDependencyGraph);
        }
    }
 
    /// <summary>
    /// Create a new solution instance without the projects specified.
    /// </summary>
    public SolutionState RemoveProjects(ArrayBuilder<ProjectId> projectIds)
    {
        Contract.ThrowIfTrue(projectIds.HasDuplicates(), "Duplicate ProjectId provided");
 
        if (projectIds.Count == 0)
            return this;
 
        foreach (var projectId in projectIds)
            CheckContainsProject(projectId);
 
        // changed project list so, increment version.
        var newSolutionAttributes = SolutionAttributes.With(version: this.Version.GetNewerVersion());
 
        using var _ = PooledHashSet<ProjectId>.GetInstance(out var projectIdsSet);
        projectIdsSet.AddRange(projectIds);
 
        var newProjectIds = ProjectIds.Where(p => !projectIdsSet.Contains(p)).ToBoxedImmutableArray();
 
        var newStateMapBuilder = ProjectStates.ToBuilder();
        foreach (var projectId in projectIds)
            newStateMapBuilder.Remove(projectId);
        var newStateMap = newStateMapBuilder.ToImmutable();
 
        // Note: it would be nice to not cause N forks of the dependency graph here.
        var newDependencyGraph = _dependencyGraph;
        foreach (var projectId in projectIds)
            newDependencyGraph = newDependencyGraph.WithProjectRemoved(projectId);
 
        var languageCountDeltas = new TemporaryArray<(string language, int count)>();
        foreach (var projectId in projectIds)
        {
            AddLanguageCountDelta(ref languageCountDeltas, ProjectStates[projectId].Language, amount: -1);
        }
 
        return this.Branch(
            solutionAttributes: newSolutionAttributes,
            projectIds: newProjectIds,
            idToProjectStateMap: newStateMap,
            projectCountByLanguage: AddLanguageCounts(ProjectCountByLanguage, languageCountDeltas),
            dependencyGraph: newDependencyGraph);
    }
 
    private static void AddLanguageCountDelta(ref TemporaryArray<(string language, int count)> languageCountDeltas, string language, int amount)
    {
        Contract.ThrowIfFalse(amount is -1 or +1);
 
        var index = languageCountDeltas.IndexOf(static (c, language) => c.language == language, language);
        if (index < 0)
        {
            languageCountDeltas.Add((language, amount));
        }
        else
        {
            languageCountDeltas[index] = (language, languageCountDeltas[index].count + amount);
        }
    }
 
    private static ImmutableDictionary<string, int> AddLanguageCounts(ImmutableDictionary<string, int> projectCountByLanguage, in TemporaryArray<(string language, int count)> languageCountDeltas)
    {
        foreach (var (language, delta) in languageCountDeltas)
        {
            if (!projectCountByLanguage.TryGetValue(language, out var currentCount))
            {
                currentCount = 0;
            }
 
            var newCount = currentCount + delta;
            if (newCount > 0)
            {
                projectCountByLanguage = projectCountByLanguage.SetItem(language, newCount);
            }
            else
            {
                Contract.ThrowIfFalse(newCount == 0);
                projectCountByLanguage = projectCountByLanguage.Remove(language);
            }
        }
 
        return projectCountByLanguage;
    }
 
    /// <summary>
    /// Creates a new solution instance with the project specified updated to have the new
    /// assembly name.
    /// </summary>
    public StateChange WithProjectAssemblyName(ProjectId projectId, string assemblyName)
    {
        var oldProject = GetRequiredProjectState(projectId);
        var newProject = oldProject.WithAssemblyName(assemblyName);
 
        if (oldProject == newProject)
        {
            return new(this, oldProject, newProject);
        }
 
        return ForkProject(oldProject, newProject);
    }
 
    /// <summary>
    /// Creates a new solution instance with the project specified updated to have the output file path.
    /// </summary>
    public StateChange WithProjectOutputFilePath(ProjectId projectId, string? outputFilePath)
    {
        var oldProject = GetRequiredProjectState(projectId);
        var newProject = oldProject.WithOutputFilePath(outputFilePath);
 
        if (oldProject == newProject)
        {
            return new(this, oldProject, newProject);
        }
 
        return ForkProject(oldProject, newProject);
    }
 
    /// <summary>
    /// Creates a new solution instance with the project specified updated to have the output file path.
    /// </summary>
    public StateChange WithProjectOutputRefFilePath(ProjectId projectId, string? outputRefFilePath)
    {
        var oldProject = GetRequiredProjectState(projectId);
        var newProject = oldProject.WithOutputRefFilePath(outputRefFilePath);
 
        if (oldProject == newProject)
        {
            return new(this, oldProject, newProject);
        }
 
        return ForkProject(oldProject, newProject);
    }
 
    /// <summary>
    /// Creates a new solution instance with the project specified updated to have the compiler output file path.
    /// </summary>
    public StateChange WithProjectCompilationOutputInfo(ProjectId projectId, in CompilationOutputInfo info)
    {
        var oldProject = GetRequiredProjectState(projectId);
        var newProject = oldProject.WithCompilationOutputInfo(info);
 
        if (oldProject == newProject)
        {
            return new(this, oldProject, newProject);
        }
 
        return ForkProject(oldProject, newProject);
    }
 
    /// <summary>
    /// Creates a new solution instance with the project specified updated to have the default namespace.
    /// </summary>
    public StateChange WithProjectDefaultNamespace(ProjectId projectId, string? defaultNamespace)
    {
        var oldProject = GetRequiredProjectState(projectId);
        var newProject = oldProject.WithDefaultNamespace(defaultNamespace);
 
        if (oldProject == newProject)
        {
            return new(this, oldProject, newProject);
        }
 
        return ForkProject(oldProject, newProject);
    }
 
    /// <summary>
    /// Creates a new solution instance with the project specified updated to have the name.
    /// </summary>
    public StateChange WithProjectChecksumAlgorithm(ProjectId projectId, SourceHashAlgorithm checksumAlgorithm)
    {
        var oldProject = GetRequiredProjectState(projectId);
        var newProject = oldProject.WithChecksumAlgorithm(checksumAlgorithm);
 
        if (oldProject == newProject)
        {
            return new(this, oldProject, newProject);
        }
 
        return ForkProject(oldProject, newProject);
    }
 
    /// <summary>
    /// Creates a new solution instance with the project specified updated to have the name.
    /// </summary>
    public StateChange WithProjectName(ProjectId projectId, string name)
    {
        var oldProject = GetRequiredProjectState(projectId);
        var newProject = oldProject.WithName(name);
 
        if (oldProject == newProject)
        {
            return new(this, oldProject, newProject);
        }
 
        return ForkProject(oldProject, newProject);
    }
 
    /// <summary>
    /// Creates a new solution instance with the project specified updated to have the project file path.
    /// </summary>
    public StateChange WithProjectFilePath(ProjectId projectId, string? filePath)
    {
        var oldProject = GetRequiredProjectState(projectId);
        var newProject = oldProject.WithFilePath(filePath);
 
        if (oldProject == newProject)
        {
            return new(this, oldProject, newProject);
        }
 
        return ForkProject(oldProject, newProject);
    }
 
    /// <summary>
    /// Create a new solution instance with the project specified updated to have
    /// the specified compilation options.
    /// </summary>
    public StateChange WithProjectCompilationOptions(ProjectId projectId, CompilationOptions? options)
    {
        var oldProject = GetRequiredProjectState(projectId);
        var newProject = oldProject.WithCompilationOptions(options);
 
        if (oldProject == newProject)
        {
            return new(this, oldProject, newProject);
        }
 
        return ForkProject(oldProject, newProject);
    }
 
    /// <summary>
    /// Create a new solution instance with the project specified updated to have
    /// the specified parse options.
    /// </summary>
    public StateChange WithProjectParseOptions(ProjectId projectId, ParseOptions? options)
    {
        var oldProject = GetRequiredProjectState(projectId);
        var newProject = oldProject.WithParseOptions(options);
 
        if (oldProject == newProject)
        {
            return new(this, oldProject, newProject);
        }
 
        return ForkProject(oldProject, newProject);
    }
 
    /// <summary>
    /// Create a new solution instance with the project specified updated to have
    /// the specified hasAllInformation.
    /// </summary>
    public StateChange WithHasAllInformation(ProjectId projectId, bool hasAllInformation)
    {
        var oldProject = GetRequiredProjectState(projectId);
        var newProject = oldProject.WithHasAllInformation(hasAllInformation);
 
        if (oldProject == newProject)
        {
            return new(this, oldProject, newProject);
        }
 
        // fork without any change on compilation.
        return ForkProject(oldProject, newProject);
    }
 
    /// <summary>
    /// Create a new solution instance with the project specified updated to have
    /// the specified runAnalyzers.
    /// </summary>
    public StateChange WithRunAnalyzers(ProjectId projectId, bool runAnalyzers)
    {
        var oldProject = GetRequiredProjectState(projectId);
        var newProject = oldProject.WithRunAnalyzers(runAnalyzers);
 
        if (oldProject == newProject)
        {
            return new(this, oldProject, newProject);
        }
 
        // fork without any change on compilation.
        return ForkProject(oldProject, newProject);
    }
 
    /// <summary>
    /// Create a new solution instance with the project specified updated to have
    /// the specified hasSdkCodeStyleAnalyzers.
    /// </summary>
    internal StateChange WithHasSdkCodeStyleAnalyzers(ProjectId projectId, bool hasSdkCodeStyleAnalyzers)
    {
        var oldProject = GetRequiredProjectState(projectId);
        var newProject = oldProject.WithHasSdkCodeStyleAnalyzers(hasSdkCodeStyleAnalyzers);
 
        if (oldProject == newProject)
        {
            return new(this, oldProject, newProject);
        }
 
        // fork without any change on compilation.
        return ForkProject(oldProject, newProject);
    }
 
    /// <summary>
    /// Create a new solution instance with the project specified updated to include
    /// the specified project references.
    /// </summary>
    public StateChange AddProjectReferences(ProjectId projectId, IReadOnlyCollection<ProjectReference> projectReferences)
    {
        var oldProject = GetRequiredProjectState(projectId);
        if (projectReferences.Count == 0)
        {
            return new(this, oldProject, oldProject);
        }
 
        var oldReferences = oldProject.ProjectReferences.ToImmutableArray();
        var newReferences = oldReferences.AddRange(projectReferences);
 
        var newProject = oldProject.WithProjectReferences(newReferences);
        var newDependencyGraph = _dependencyGraph.WithAdditionalProjectReferences(projectId, projectReferences);
 
        return ForkProject(oldProject, newProject, newDependencyGraph: newDependencyGraph);
    }
 
    /// <summary>
    /// Create a new solution instance with the project specified updated to no longer
    /// include the specified project reference.
    /// </summary>
    public StateChange RemoveProjectReference(ProjectId projectId, ProjectReference projectReference)
    {
        var oldProject = GetRequiredProjectState(projectId);
        var oldReferences = oldProject.ProjectReferences.ToImmutableArray();
 
        // Note: uses ProjectReference equality to compare references.
        var newReferences = oldReferences.Remove(projectReference);
 
        if (oldReferences == newReferences)
        {
            return new(this, oldProject, oldProject);
        }
 
        var newProject = oldProject.WithProjectReferences(newReferences);
 
        ProjectDependencyGraph newDependencyGraph;
        if (newProject.ContainsReferenceToProject(projectReference.ProjectId) ||
            !ProjectStates.ContainsKey(projectReference.ProjectId))
        {
            // Two cases:
            // 1) The project contained multiple non-equivalent references to the project,
            // and not all of them were removed. The dependency graph doesn't change.
            // Note that there might be two references to the same project, one with
            // extern alias and the other without. These are not considered duplicates.
            // 2) The referenced project is not part of the solution and hence not included
            // in the dependency graph.
            newDependencyGraph = _dependencyGraph;
        }
        else
        {
            newDependencyGraph = _dependencyGraph.WithProjectReferenceRemoved(projectId, projectReference.ProjectId);
        }
 
        return ForkProject(oldProject, newProject, newDependencyGraph: newDependencyGraph);
    }
 
    /// <summary>
    /// Create a new solution instance with the project specified updated to contain
    /// the specified list of project references.
    /// </summary>
    public StateChange WithProjectReferences(ProjectId projectId, IReadOnlyList<ProjectReference> projectReferences)
    {
        var oldProject = GetRequiredProjectState(projectId);
        var newProject = oldProject.WithProjectReferences(projectReferences);
        if (oldProject == newProject)
        {
            return new(this, oldProject, newProject);
        }
 
        var newDependencyGraph = _dependencyGraph.WithProjectReferences(projectId, projectReferences);
        return ForkProject(oldProject, newProject, newDependencyGraph: newDependencyGraph);
    }
 
    /// <summary>
    /// Creates a new solution instance with the project documents in the order by the specified document ids.
    /// The specified document ids must be the same as what is already in the project; no adding or removing is allowed.
    /// </summary>
    public StateChange WithProjectDocumentsOrder(ProjectId projectId, ImmutableList<DocumentId> documentIds)
    {
        var oldProject = GetRequiredProjectState(projectId);
 
        if (documentIds.Count != oldProject.DocumentStates.Count)
        {
            throw new ArgumentException($"The specified documents do not equal the project document count.", nameof(documentIds));
        }
 
        foreach (var id in documentIds)
        {
            if (!oldProject.DocumentStates.Contains(id))
            {
                throw new InvalidOperationException($"The document '{id}' does not exist in the project.");
            }
        }
 
        var newProject = oldProject.UpdateDocumentsOrder(documentIds);
 
        if (oldProject == newProject)
        {
            return new(this, oldProject, newProject);
        }
 
        return ForkProject(oldProject, newProject);
    }
 
    /// <summary>
    /// Create a new solution instance with the project specified updated to include the
    /// specified metadata references.
    /// </summary>
    public StateChange AddMetadataReferences(ProjectId projectId, IReadOnlyCollection<MetadataReference> metadataReferences)
    {
        var oldProject = GetRequiredProjectState(projectId);
        if (metadataReferences.Count == 0)
        {
            return new(this, oldProject, oldProject);
        }
 
        var oldReferences = oldProject.MetadataReferences.ToImmutableArray();
        var newReferences = oldReferences.AddRange(metadataReferences);
 
        return ForkProject(oldProject, oldProject.WithMetadataReferences(newReferences));
    }
 
    /// <summary>
    /// Create a new solution instance with the project specified updated to no longer include
    /// the specified metadata reference.
    /// </summary>
    public StateChange RemoveMetadataReference(ProjectId projectId, MetadataReference metadataReference)
    {
        var oldProject = GetRequiredProjectState(projectId);
        var oldReferences = oldProject.MetadataReferences.ToImmutableArray();
        var newReferences = oldReferences.Remove(metadataReference);
        if (oldReferences == newReferences)
        {
            return new(this, oldProject, oldProject);
        }
 
        return ForkProject(oldProject, oldProject.WithMetadataReferences(newReferences));
    }
 
    /// <summary>
    /// Create a new solution instance with the project specified updated to include only the
    /// specified metadata references.
    /// </summary>
    public StateChange WithProjectMetadataReferences(ProjectId projectId, IReadOnlyList<MetadataReference> metadataReferences)
    {
        var oldProject = GetRequiredProjectState(projectId);
        var newProject = oldProject.WithMetadataReferences(metadataReferences);
        if (oldProject == newProject)
        {
            return new(this, oldProject, newProject);
        }
 
        return ForkProject(oldProject, newProject);
    }
 
    /// <summary>
    /// Create a new solution instance with the project specified updated to include only the
    /// specified analyzer references.
    /// </summary>
    public StateChange WithProjectAnalyzerReferences(ProjectId projectId, IReadOnlyList<AnalyzerReference> analyzerReferences)
    {
        var oldProject = GetRequiredProjectState(projectId);
        var newProject = oldProject.WithAnalyzerReferences(analyzerReferences);
        if (oldProject == newProject)
        {
            return new(this, oldProject, newProject);
        }
 
        return ForkProject(oldProject, newProject);
    }
 
    /// <summary>
    /// Creates a new solution instance with updated analyzer fallback options.
    /// </summary>
    public SolutionState WithFallbackAnalyzerOptions(ImmutableDictionary<string, StructuredAnalyzerConfigOptions> options)
    {
        if (FallbackAnalyzerOptions == options)
        {
            return this;
        }
 
        var newProjectStatesMap = ProjectStates.ToImmutableDictionary(
            keySelector: static entry => entry.Key,
            elementSelector: entry =>
            {
                // If the new options are specified for the project language we use them,
                // otherwise we clear the options for the project.
                if (!options.TryGetValue(entry.Value.Language, out var languageOptions))
                {
                    languageOptions = StructuredAnalyzerConfigOptions.Empty;
                }
 
                return entry.Value.WithFallbackAnalyzerOptions(languageOptions);
            });
 
        return Branch(
            fallbackAnalyzerOptions: options,
            idToProjectStateMap: newProjectStatesMap);
    }
 
    /// <summary>
    /// Creates a new solution instance with an attribute of the document updated, if its value has changed.
    /// </summary>
    public StateChange WithDocumentAttributes<TArg>(
        DocumentId documentId,
        TArg arg,
        Func<DocumentInfo.DocumentAttributes, TArg, DocumentInfo.DocumentAttributes> updateAttributes)
    {
        var oldDocument = GetRequiredDocumentState(documentId);
 
        var newDocument = oldDocument.WithAttributes(updateAttributes(oldDocument.Attributes, arg));
        if (ReferenceEquals(oldDocument, newDocument))
        {
            var oldProject = GetRequiredProjectState(documentId.ProjectId);
            return new(this, oldProject, oldProject);
        }
 
        return UpdateDocumentState(newDocument);
    }
 
    /// <summary>
    /// Creates a new solution instance with the document specified updated to have the text
    /// specified.
    /// </summary>
    public StateChange WithDocumentText(DocumentId documentId, SourceText text, PreservationMode mode = PreservationMode.PreserveValue)
    {
        var oldDocument = GetRequiredDocumentState(documentId);
        if (oldDocument.TryGetText(out var oldText) && text == oldText)
        {
            var oldProject = GetRequiredProjectState(documentId.ProjectId);
            return new(this, oldProject, oldProject);
        }
 
        return UpdateDocumentState(oldDocument.UpdateText(text, mode));
    }
 
    public StateChange WithDocumentState(DocumentState newDocument)
    {
        var oldDocument = GetRequiredDocumentState(newDocument.Id);
        if (oldDocument == newDocument)
        {
            var oldProject = GetRequiredProjectState(newDocument.Id.ProjectId);
            return new(this, oldProject, oldProject);
        }
 
        return UpdateDocumentState(newDocument);
    }
 
    /// <summary>
    /// Creates a new solution instance with the additional document specified updated to have the text
    /// specified.
    /// </summary>
    public StateChange WithAdditionalDocumentText(DocumentId documentId, SourceText text, PreservationMode mode = PreservationMode.PreserveValue)
    {
        var oldDocument = GetRequiredAdditionalDocumentState(documentId);
        if (oldDocument.TryGetText(out var oldText) && text == oldText)
        {
            var oldProject = GetRequiredProjectState(documentId.ProjectId);
            return new(this, oldProject, oldProject);
        }
 
        return UpdateAdditionalDocumentState(oldDocument.UpdateText(text, mode));
    }
 
    /// <summary>
    /// Creates a new solution instance with the document specified updated to have the text
    /// specified.
    /// </summary>
    public StateChange WithAnalyzerConfigDocumentText(DocumentId documentId, SourceText text, PreservationMode mode = PreservationMode.PreserveValue)
    {
        var oldDocument = GetRequiredAnalyzerConfigDocumentState(documentId);
        if (oldDocument.TryGetText(out var oldText) && text == oldText)
        {
            var oldProject = GetRequiredProjectState(documentId.ProjectId);
            return new(this, oldProject, oldProject);
        }
 
        return UpdateAnalyzerConfigDocumentState(oldDocument.UpdateText(text, mode));
    }
 
    /// <summary>
    /// Creates a new solution instance with the document specified updated to have the text
    /// and version specified.
    /// </summary>
    public StateChange WithDocumentText(DocumentId documentId, TextAndVersion textAndVersion, PreservationMode mode = PreservationMode.PreserveValue)
    {
        var oldDocument = GetRequiredDocumentState(documentId);
        if (oldDocument.TryGetTextAndVersion(out var oldTextAndVersion) && textAndVersion == oldTextAndVersion)
        {
            var oldProject = GetRequiredProjectState(documentId.ProjectId);
            return new(this, oldProject, oldProject);
        }
 
        return UpdateDocumentState(oldDocument.UpdateText(textAndVersion, mode));
    }
 
    /// <summary>
    /// Creates a new solution instance with the additional document specified updated to have the text
    /// and version specified.
    /// </summary>
    public StateChange WithAdditionalDocumentText(DocumentId documentId, TextAndVersion textAndVersion, PreservationMode mode = PreservationMode.PreserveValue)
    {
        var oldDocument = GetRequiredAdditionalDocumentState(documentId);
        if (oldDocument.TryGetTextAndVersion(out var oldTextAndVersion) && textAndVersion == oldTextAndVersion)
        {
            var oldProject = GetRequiredProjectState(documentId.ProjectId);
            return new(this, oldProject, oldProject);
        }
 
        return UpdateAdditionalDocumentState(oldDocument.UpdateText(textAndVersion, mode));
    }
 
    /// <summary>
    /// Creates a new solution instance with the analyzer config document specified updated to have the text
    /// and version specified.
    /// </summary>
    public StateChange WithAnalyzerConfigDocumentText(DocumentId documentId, TextAndVersion textAndVersion, PreservationMode mode = PreservationMode.PreserveValue)
    {
        var oldDocument = GetRequiredAnalyzerConfigDocumentState(documentId);
        if (oldDocument.TryGetTextAndVersion(out var oldTextAndVersion) && textAndVersion == oldTextAndVersion)
        {
            var oldProject = GetRequiredProjectState(documentId.ProjectId);
            return new(this, oldProject, oldProject);
        }
 
        return UpdateAnalyzerConfigDocumentState(oldDocument.UpdateText(textAndVersion, mode));
    }
 
    /// <summary>
    /// Creates a new solution instance with the document specified updated to have the source
    /// code kind specified.
    /// </summary>
    public StateChange WithDocumentSourceCodeKind(DocumentId documentId, SourceCodeKind sourceCodeKind)
    {
        var oldDocument = GetRequiredDocumentState(documentId);
        if (oldDocument.SourceCodeKind == sourceCodeKind)
        {
            var oldProject = GetRequiredProjectState(documentId.ProjectId);
            return new(this, oldProject, oldProject);
        }
 
        return UpdateDocumentState(oldDocument.UpdateSourceCodeKind(sourceCodeKind));
    }
 
    public StateChange UpdateDocumentTextLoader(DocumentId documentId, TextLoader loader, PreservationMode mode)
    {
        var oldDocument = GetRequiredDocumentState(documentId);
 
        // Assumes that content has changed. User could have closed a doc without saving and we are loading text
        // from closed file with old content.
        return UpdateDocumentState(oldDocument.UpdateText(loader, mode));
    }
 
    /// <summary>
    /// Creates a new solution instance with the additional document specified updated to have the text
    /// supplied by the text loader.
    /// </summary>
    public StateChange UpdateAdditionalDocumentTextLoader(DocumentId documentId, TextLoader loader, PreservationMode mode)
    {
        var oldDocument = GetRequiredAdditionalDocumentState(documentId);
 
        // Assumes that content has changed. User could have closed a doc without saving and we are loading text
        // from closed file with old content.
        return UpdateAdditionalDocumentState(oldDocument.UpdateText(loader, mode));
    }
 
    /// <summary>
    /// Creates a new solution instance with the analyzer config document specified updated to have the text
    /// supplied by the text loader.
    /// </summary>
    public StateChange UpdateAnalyzerConfigDocumentTextLoader(DocumentId documentId, TextLoader loader, PreservationMode mode)
    {
        var oldDocument = GetRequiredAnalyzerConfigDocumentState(documentId);
 
        // Assumes that text has changed. User could have closed a doc without saving and we are loading text from closed file with
        // old content. Also this should make sure we don't re-use latest doc version with data associated with opened document.
        return UpdateAnalyzerConfigDocumentState(oldDocument.UpdateText(loader, mode));
    }
 
    private StateChange UpdateDocumentState(DocumentState newDocument)
    {
        var oldProject = GetRequiredProjectState(newDocument.Id.ProjectId);
        var newProject = oldProject.UpdateDocument(newDocument);
 
        // This method shouldn't have been called if the document has not changed.
        Debug.Assert(oldProject != newProject);
 
        return ForkProject(
            oldProject,
            newProject);
    }
 
    private StateChange UpdateAdditionalDocumentState(AdditionalDocumentState newDocument)
    {
        var oldProject = GetRequiredProjectState(newDocument.Id.ProjectId);
        var newProject = oldProject.UpdateAdditionalDocument(newDocument);
 
        // This method shouldn't have been called if the document has not changed.
        Debug.Assert(oldProject != newProject);
 
        return ForkProject(oldProject, newProject);
    }
 
    private StateChange UpdateAnalyzerConfigDocumentState(AnalyzerConfigDocumentState newDocument)
    {
        var oldProject = GetRequiredProjectState(newDocument.Id.ProjectId);
        var newProject = oldProject.UpdateAnalyzerConfigDocument(newDocument);
 
        // This method shouldn't have been called if the document has not changed.
        Debug.Assert(oldProject != newProject);
 
        return ForkProject(oldProject, newProject);
    }
 
    /// <summary>
    /// Creates a new snapshot with an updated project and an action that will produce a new
    /// compilation matching the new project out of an old compilation. All dependent projects
    /// are fixed-up if the change to the new project affects its public metadata, and old
    /// dependent compilations are forgotten.
    /// </summary>
    public StateChange ForkProject(
        ProjectState oldProjectState,
        ProjectState newProjectState,
        ProjectDependencyGraph? newDependencyGraph = null)
    {
        var projectId = newProjectState.Id;
 
        Contract.ThrowIfFalse(ProjectStates.ContainsKey(projectId));
        var newStateMap = ProjectStates.SetItem(projectId, newProjectState);
 
        newDependencyGraph ??= _dependencyGraph;
 
        var newSolutionState = this.Branch(
            idToProjectStateMap: newStateMap,
            dependencyGraph: newDependencyGraph);
 
        return new(newSolutionState, oldProjectState, newProjectState);
    }
 
    /// <inheritdoc cref="Solution.GetDocumentIdsWithFilePath(string?)" />
    public ImmutableArray<DocumentId> GetDocumentIdsWithFilePath(string? filePath)
    {
        if (string.IsNullOrEmpty(filePath))
            return [];
 
        return ImmutableInterlocked.GetOrAdd(
            ref _lazyFilePathToRelatedDocumentIds,
            filePath,
            static (filePath, @this) => ComputeDocumentIdsWithFilePath(@this, filePath),
            this);
 
        static ImmutableArray<DocumentId> ComputeDocumentIdsWithFilePath(SolutionState @this, string filePath)
        {
            using var result = TemporaryArray<DocumentId>.Empty;
            foreach (var (projectId, projectState) in @this.ProjectStates)
                projectState.AddDocumentIdsWithFilePath(ref result.AsRef(), filePath);
 
            return result.ToImmutableAndClear();
        }
    }
 
    public static ProjectDependencyGraph CreateDependencyGraph(
        IReadOnlyList<ProjectId> projectIds,
        ImmutableDictionary<ProjectId, ProjectState> projectStates)
    {
        var map = projectStates.Values.Select(state => KeyValuePairUtil.Create(
                state.Id,
                state.ProjectReferences.Where(pr => projectStates.ContainsKey(pr.ProjectId)).Select(pr => pr.ProjectId).ToImmutableHashSet()))
                .ToImmutableDictionary();
 
        return new ProjectDependencyGraph([.. projectIds], map);
    }
 
    public SolutionState WithOptions(SolutionOptionSet options)
        => Branch(options: options);
 
    public SolutionState AddAnalyzerReferences(IReadOnlyCollection<AnalyzerReference> analyzerReferences)
    {
        if (analyzerReferences.Count == 0)
        {
            return this;
        }
 
        var oldReferences = AnalyzerReferences.ToImmutableArray();
        var newReferences = oldReferences.AddRange(analyzerReferences);
        return Branch(analyzerReferences: newReferences);
    }
 
    public SolutionState RemoveAnalyzerReference(AnalyzerReference analyzerReference)
    {
        var oldReferences = AnalyzerReferences.ToImmutableArray();
        var newReferences = oldReferences.Remove(analyzerReference);
        if (oldReferences == newReferences)
        {
            return this;
        }
 
        return Branch(analyzerReferences: newReferences);
    }
 
    public SolutionState WithAnalyzerReferences(IReadOnlyList<AnalyzerReference> analyzerReferences)
    {
        if (analyzerReferences == AnalyzerReferences)
        {
            return this;
        }
 
        return Branch(analyzerReferences: analyzerReferences);
    }
 
    public DocumentId? GetFirstRelatedDocumentId(DocumentId documentId, ProjectId? relatedProjectIdHint)
    {
        Contract.ThrowIfTrue(documentId.ProjectId == relatedProjectIdHint);
 
        var projectState = this.GetProjectState(documentId.ProjectId);
        if (projectState is null)
            return null;
 
        var documentState = projectState.DocumentStates.GetState(documentId);
        if (documentState is null)
            return null;
 
        var filePath = documentState.FilePath;
        if (string.IsNullOrEmpty(filePath))
            return null;
 
        // Do a quick check if the full info for that path has already been computed and cached.
        var fileMap = _lazyFilePathToRelatedDocumentIds;
        if (fileMap != null && fileMap.TryGetValue(filePath, out var relatedDocumentIds))
        {
            foreach (var relatedDocumentId in relatedDocumentIds)
            {
                if (relatedDocumentId != documentId)
                    return relatedDocumentId;
            }
 
            return null;
        }
 
        var relatedProject = relatedProjectIdHint is null ? null : this.ProjectStates[relatedProjectIdHint];
        Contract.ThrowIfTrue(relatedProject == projectState);
        if (relatedProject != null)
        {
            var siblingDocumentId = relatedProject.GetFirstDocumentIdWithFilePath(filePath);
            if (siblingDocumentId is not null)
                return siblingDocumentId;
        }
 
        // Wasn't in cache, do the linear search.
        foreach (var (_, siblingProjectState) in this.ProjectStates)
        {
            // Don't want to search the same project that document already came from, or from the related-project we had a hint for.
            if (siblingProjectState == projectState || siblingProjectState == relatedProject)
                continue;
 
            var siblingDocumentId = siblingProjectState.GetFirstDocumentIdWithFilePath(filePath);
            if (siblingDocumentId is not null)
                return siblingDocumentId;
        }
 
        return null;
    }
 
    public ImmutableArray<DocumentId> GetRelatedDocumentIds(DocumentId documentId, bool includeDifferentLanguages)
    {
        var projectState = this.GetProjectState(documentId.ProjectId);
        if (projectState == null)
        {
            // this document no longer exist
            return [];
        }
 
        var documentState = projectState.DocumentStates.GetState(documentId);
        if (documentState == null)
        {
            // this document no longer exist
            return [];
        }
 
        var filePath = documentState.FilePath;
        if (string.IsNullOrEmpty(filePath))
        {
            // this document can't have any related document. only related document is itself.
            return [documentId];
        }
 
        var documentIds = GetDocumentIdsWithFilePath(filePath);
        return documentIds.WhereAsArray(
            static (documentId, args) =>
            {
                var (@this, language, includeDifferentLanguages) = args;
 
                var projectState = @this.GetProjectState(documentId.ProjectId);
                if (projectState == null)
                {
                    // this document no longer exist
                    // I'm adding this ReportAndCatch to see if this does happen in the wild; it's not clear to me under what scenario that could happen since all the IDs of all document types
                    // should be removed when a project is removed.
                    FatalError.ReportAndCatch(new Exception("GetDocumentIdsWithFilePath returned a document in a project that does not exist."));
                    return false;
                }
 
                if (!includeDifferentLanguages && projectState.ProjectInfo.Language != language)
                    return false;
 
                // GetDocumentIdsWithFilePath may return DocumentIds for other types of documents (like additional files), so filter to normal documents
                return projectState.DocumentStates.Contains(documentId);
            },
            (solution: this, projectState.Language, includeDifferentLanguages));
    }
 
    /// <summary>
    /// Gets a <see cref="ProjectDependencyGraph"/> that details the dependencies between projects for this solution.
    /// </summary>
    public ProjectDependencyGraph GetProjectDependencyGraph()
        => _dependencyGraph;
 
    private void CheckNotContainsProject(ProjectId projectId)
    {
        if (this.ContainsProject(projectId))
        {
            throw new InvalidOperationException(WorkspacesResources.The_solution_already_contains_the_specified_project);
        }
    }
 
    internal void CheckContainsProject(ProjectId projectId)
    {
        if (!this.ContainsProject(projectId))
        {
            throw new InvalidOperationException(WorkspacesResources.The_solution_does_not_contain_the_specified_project);
        }
    }
 
    internal bool ContainsProjectReference(ProjectId projectId, ProjectReference projectReference)
        => GetRequiredProjectState(projectId).ProjectReferences.Contains(projectReference);
 
    internal bool ContainsMetadataReference(ProjectId projectId, MetadataReference metadataReference)
        => GetRequiredProjectState(projectId).MetadataReferences.Contains(metadataReference);
 
    internal bool ContainsAnalyzerReference(ProjectId projectId, AnalyzerReference analyzerReference)
        => GetRequiredProjectState(projectId).AnalyzerReferences.Contains(analyzerReference);
 
    internal bool ContainsTransitiveReference(ProjectId fromProjectId, ProjectId toProjectId)
        => _dependencyGraph.GetProjectsThatThisProjectTransitivelyDependsOn(fromProjectId).Contains(toProjectId);
}