File: AddMissingReference\AddMissingReferenceCodeAction.cs
Web Access
Project: src\src\Features\Core\Portable\Microsoft.CodeAnalysis.Features.csproj (Microsoft.CodeAnalysis.Features)
// 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.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeActions.WorkspaceServices;
using Microsoft.CodeAnalysis.Shared.Extensions;
 
namespace Microsoft.CodeAnalysis.AddMissingReference;
 
internal sealed class AddMissingReferenceCodeAction(Project project, string title, ProjectReference? projectReferenceToAdd, AssemblyIdentity missingAssemblyIdentity) : CodeAction
{
    private readonly Project _project = project;
    private readonly ProjectReference? _projectReferenceToAdd = projectReferenceToAdd;
    private readonly AssemblyIdentity _missingAssemblyIdentity = missingAssemblyIdentity;
 
    public override string Title { get; } = title;
 
    /// <summary>
    /// This code action only works by adding references.  As such, it requires a non document change (and is
    /// thus restricted in which hosts it can run).
    /// </summary>
    public override ImmutableArray<string> Tags => RequiresNonDocumentChangeTags;
 
    public static async Task<CodeAction> CreateAsync(Project project, AssemblyIdentity missingAssemblyIdentity, CancellationToken cancellationToken)
    {
        var dependencyGraph = project.Solution.GetProjectDependencyGraph();
 
        // We want to find a project that generates this assembly, if one so exists. We therefore 
        // search all projects that our project with an error depends on. We want to do this for
        // complicated and evil scenarios like this one:
        //
        //     C -> B -> A
        //
        //     A'
        //
        // Where, for some insane reason, A and A' are two projects that both emit an assembly 
        // by the same name. So imagine we are using a type in B from C, and we are missing a 
        // reference to A.dll. Both A and A' are candidates, but we know we can throw out A'
        // since whatever type from B we are using that's causing the error, we know that type 
        // isn't referencing A'. Put another way: this code action adds a reference, but should 
        // never change the transitive closure of project references that C has.
        //
        // Doing this filtering also means we get to check less projects (good), and ensures that
        // whatever project reference we end up adding won't add a circularity (also good.)
        foreach (var candidateProjectId in dependencyGraph.GetProjectsThatThisProjectTransitivelyDependsOn(project.Id))
        {
            var candidateProject = project.Solution.GetRequiredProject(candidateProjectId);
            if (candidateProject.SupportsCompilation &&
                string.Equals(missingAssemblyIdentity.Name, candidateProject.AssemblyName, StringComparison.OrdinalIgnoreCase))
            {
                // The name matches, so let's see if the full identities are equal. 
                var compilation = await candidateProject.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false);
                if (missingAssemblyIdentity.Equals(compilation.Assembly.Identity))
                {
                    // It matches, so just add a reference to this
                    return new AddMissingReferenceCodeAction(project,
                        string.Format(FeaturesResources.Add_project_reference_to_0, candidateProject.Name),
                        new ProjectReference(candidateProjectId), missingAssemblyIdentity);
                }
            }
        }
 
        // No matching project, so metadata reference
        var description = string.Format(FeaturesResources.Add_reference_to_0, missingAssemblyIdentity.GetDisplayName());
        return new AddMissingReferenceCodeAction(project, description, null, missingAssemblyIdentity);
    }
 
    protected override Task<ImmutableArray<CodeActionOperation>> ComputeOperationsAsync(
        IProgress<CodeAnalysisProgress> progress, CancellationToken cancellationToken)
    {
        // If we have a project reference to add, then add it
        if (_projectReferenceToAdd != null)
        {
            // note: no need to post process since we are just adding a project reference and not making any code changes.
            return Task.FromResult(ImmutableArray.Create<CodeActionOperation>(
                new ApplyChangesOperation(_project.AddProjectReference(_projectReferenceToAdd).Solution)));
        }
        else
        {
            // We didn't have any project, so we need to try adding a metadata reference
            var factoryService = _project.Solution.Services.GetRequiredService<IAddMetadataReferenceCodeActionOperationFactoryWorkspaceService>();
            var operation = factoryService.CreateAddMetadataReferenceOperation(_project.Id, _missingAssemblyIdentity);
            return Task.FromResult(ImmutableArray.Create(operation));
        }
    }
}