File: Workspace\Solution\ProjectDependencyGraph_AddProjectReference.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.Linq;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis;
 
public partial class ProjectDependencyGraph
{
    internal ProjectDependencyGraph WithAdditionalProjectReferences(ProjectId projectId, IReadOnlyCollection<ProjectReference> projectReferences)
    {
        Contract.ThrowIfFalse(ProjectIds.Contains(projectId));
 
        if (projectReferences.Count == 0)
        {
            return this;
        }
 
        // only add references to projects that are contained in the solution/graph
        var referencedProjectIds = projectReferences
            .Where(r => ProjectIds.Contains(r.ProjectId))
            .Select(r => r.ProjectId)
            .ToList();
 
        if (referencedProjectIds.Count == 0)
        {
            return this;
        }
 
        var newReferencesMap = ComputeNewReferencesMapForAdditionalProjectReferences(_referencesMap, projectId, referencedProjectIds);
 
        var newReverseReferencesMap = ComputeNewReverseReferencesMapForAdditionalProjectReferences(_lazyReverseReferencesMap, projectId, referencedProjectIds);
 
        var newTransitiveReferencesMap = ComputeNewTransitiveReferencesMapForAdditionalProjectReferences(_transitiveReferencesMap, projectId, referencedProjectIds);
 
        var newReverseTransitiveReferencesMap = ComputeNewReverseTransitiveReferencesMapForAdditionalProjectReferences(_reverseTransitiveReferencesMap, projectId, referencedProjectIds);
 
        // Note: rather than updating our dependency sets and topologically sorted data, we'll throw that away since incremental update is
        // tricky, and those are rarely used. If somebody needs them, it'll be lazily computed.
        return new ProjectDependencyGraph(
            ProjectIds,
            referencesMap: newReferencesMap,
            reverseReferencesMap: newReverseReferencesMap,
            transitiveReferencesMap: newTransitiveReferencesMap,
            reverseTransitiveReferencesMap: newReverseTransitiveReferencesMap,
            topologicallySortedProjects: default,
            dependencySets: default);
    }
 
    /// <summary>
    /// Computes a new <see cref="_referencesMap"/> for the addition of additional project references.
    /// </summary>
    private static ImmutableDictionary<ProjectId, ImmutableHashSet<ProjectId>> ComputeNewReferencesMapForAdditionalProjectReferences(
        ImmutableDictionary<ProjectId, ImmutableHashSet<ProjectId>> existingReferencesMap,
        ProjectId projectId,
        IReadOnlyList<ProjectId> referencedProjectIds)
    {
        if (existingReferencesMap.TryGetValue(projectId, out var existingReferences))
        {
            return existingReferencesMap.SetItem(projectId, existingReferences.Union(referencedProjectIds));
        }
        else
        {
            return existingReferencesMap.SetItem(projectId, [.. referencedProjectIds]);
        }
    }
 
    /// <summary>
    /// Computes a new <see cref="_lazyReverseReferencesMap"/> for the addition of additional project references.
    /// </summary>
    /// <param name="existingReverseReferencesMap">The previous <see cref="_lazyReverseReferencesMap"/>, or
    /// <see langword="null"/> if the reverse references map was not computed for the previous graph.</param>
    private static ImmutableDictionary<ProjectId, ImmutableHashSet<ProjectId>>? ComputeNewReverseReferencesMapForAdditionalProjectReferences(
        ImmutableDictionary<ProjectId, ImmutableHashSet<ProjectId>>? existingReverseReferencesMap,
        ProjectId projectId,
        IReadOnlyList<ProjectId> referencedProjectIds)
    {
        if (existingReverseReferencesMap is null)
            return null;
 
        var builder = existingReverseReferencesMap.ToBuilder();
 
        foreach (var referencedProject in referencedProjectIds)
        {
            builder.MultiAdd(referencedProject, projectId);
        }
 
        return builder.ToImmutable();
    }
 
    /// <summary>
    /// Computes a new <see cref="_transitiveReferencesMap"/> for the addition of additional project references. 
    /// </summary>
    private static ImmutableDictionary<ProjectId, ImmutableHashSet<ProjectId>> ComputeNewTransitiveReferencesMapForAdditionalProjectReferences(
        ImmutableDictionary<ProjectId, ImmutableHashSet<ProjectId>> existingTransitiveReferencesMap,
        ProjectId projectId,
        IReadOnlyList<ProjectId> referencedProjectIds)
    {
        // To update our forward transitive map, we need to add referencedProjectIds (and their transitive dependencies) to the transitive references
        // of projects. First, let's just compute the new set of transitive references. It's possible while doing so we'll discover that we don't
        // know the transitive project references for one of our new references. In that case, we'll use null as a sentinel to mean "we don't know" and
        // we propagate the not-knowingness. But let's not worry about that yet. First, let's just get the new transitive reference set.
        var newTransitiveReferences = new HashSet<ProjectId>(referencedProjectIds);
 
        foreach (var referencedProjectId in referencedProjectIds)
        {
            if (existingTransitiveReferencesMap.TryGetValue(referencedProjectId, out var additionalTransitiveReferences))
            {
                newTransitiveReferences.UnionWith(additionalTransitiveReferences);
            }
            else
            {
                newTransitiveReferences = null;
                break;
            }
        }
 
        // We'll now loop through each entry in our existing cache and compute updates. We'll accumulate them into this builder.
        var builder = existingTransitiveReferencesMap.ToBuilder();
 
        foreach (var projectIdToUpdate in existingTransitiveReferencesMap.Keys)
        {
            existingTransitiveReferencesMap.TryGetValue(projectIdToUpdate, out var existingTransitiveReferences);
 
            // The projects who need to have their caches updated are projectIdToUpdate (since we're obviously updating it!)
            // and also anything that depended on it.
            if (projectIdToUpdate == projectId || existingTransitiveReferences?.Contains(projectId) == true)
            {
                // This needs an update. If we know what to include in, we'll union it with the existing ones. Otherwise, we don't know
                // and we'll remove any data from the cache.
                if (newTransitiveReferences != null && existingTransitiveReferences != null)
                {
                    builder[projectIdToUpdate] = existingTransitiveReferences.Union(newTransitiveReferences);
                }
                else
                {
                    // Either we don't know the full set of the new references being added, or don't know the existing set projectIdToUpdate.
                    // In this case, just remove it
                    builder.Remove(projectIdToUpdate);
                }
            }
        }
 
        return builder.ToImmutable();
    }
 
    /// <summary>
    /// Computes a new <see cref="_reverseTransitiveReferencesMap"/> for the addition of new projects.
    /// </summary>
    private static ImmutableDictionary<ProjectId, ImmutableHashSet<ProjectId>> ComputeNewReverseTransitiveReferencesMapForAdditionalProjectReferences(
        ImmutableDictionary<ProjectId, ImmutableHashSet<ProjectId>> existingReverseTransitiveReferencesMap,
        ProjectId projectId,
        IReadOnlyList<ProjectId> referencedProjectIds)
    {
        // To update the reverse transitive map, we need to add the existing reverse transitive references of projectId to any of referencedProjectIds,
        // and anything else with a reverse dependency on them. If we don't already know our reverse transitive references, then we'll have to instead remove
        // the cache entries instead of update them. We'll fetch this from the map, and use "null" to indicate the "we don't know and should remove the cache entry"
        // instead
        existingReverseTransitiveReferencesMap.TryGetValue(projectId, out var newReverseTranstiveReferences);
 
        if (newReverseTranstiveReferences != null)
        {
            newReverseTranstiveReferences = newReverseTranstiveReferences.Add(projectId);
        }
 
        // We'll now loop through each entry in our existing cache and compute updates. We'll accumulate them into this builder.
        var builder = existingReverseTransitiveReferencesMap.ToBuilder();
 
        foreach (var projectIdToUpdate in existingReverseTransitiveReferencesMap.Keys)
        {
            existingReverseTransitiveReferencesMap.TryGetValue(projectIdToUpdate, out var existingReverseTransitiveReferences);
 
            // The projects who need to have their caches updated are projectIdToUpdate (since we're obviously updating it!)
            // and also anything that depended on us.
            if (referencedProjectIds.Contains(projectIdToUpdate) || existingReverseTransitiveReferences?.Overlaps(referencedProjectIds) == true)
            {
                // This needs an update. If we know what to include in, we'll union it with the existing ones. Otherwise, we don't know
                // and we'll remove any data from the cache.
                if (newReverseTranstiveReferences != null && existingReverseTransitiveReferences != null)
                {
                    builder[projectIdToUpdate] = existingReverseTransitiveReferences.Union(newReverseTranstiveReferences);
                }
                else
                {
                    // Either we don't know the full set of the new references being added, or don't know the existing set projectIdToUpdate.
                    // In this case, just remove it
                    builder.Remove(projectIdToUpdate);
                }
            }
        }
 
        return builder.ToImmutable();
    }
}