File: MSBuild\MSBuildProjectLoader.Worker_ResolveReferences.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.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.PooledObjects;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.MSBuild
{
    public partial class MSBuildProjectLoader
    {
        private partial class Worker
        {
            private readonly struct ResolvedReferences
            {
                public ImmutableHashSet<ProjectReference> ProjectReferences { get; }
                public ImmutableArray<MetadataReference> MetadataReferences { get; }
 
                public ResolvedReferences(ImmutableHashSet<ProjectReference> projectReferences, ImmutableArray<MetadataReference> metadataReferences)
                {
                    ProjectReferences = projectReferences;
                    MetadataReferences = metadataReferences;
                }
            }
 
            /// <summary>
            /// This type helps produces lists of metadata and project references. Initially, it contains a list of metadata references.
            /// As project references are added, the metadata references that match those project references are removed.
            /// </summary>
            private class ResolvedReferencesBuilder
            {
                /// <summary>
                /// The full list of <see cref="MetadataReference"/>s.
                /// </summary>
                private readonly ImmutableArray<MetadataReference> _metadataReferences;
 
                /// <summary>
                /// A map of every metadata reference file paths to a set of indices whether than file path
                /// exists in the list. It is expected that there may be multiple metadata references for the
                /// same file path in the case where multiple extern aliases are provided.
                /// </summary>
                private readonly ImmutableDictionary<string, HashSet<int>> _pathToIndicesMap;
 
                /// <summary>
                /// A set of indices into <see cref="_metadataReferences"/> that are to be removed.
                /// </summary>
                private readonly HashSet<int> _indicesToRemove;
 
                private readonly ImmutableHashSet<ProjectReference>.Builder _projectReferences;
 
                public ResolvedReferencesBuilder(IEnumerable<MetadataReference> metadataReferences)
                {
                    _metadataReferences = metadataReferences.ToImmutableArray();
                    _pathToIndicesMap = CreatePathToIndexMap(_metadataReferences);
                    _indicesToRemove = [];
                    _projectReferences = ImmutableHashSet.CreateBuilder<ProjectReference>();
                }
 
                private static ImmutableDictionary<string, HashSet<int>> CreatePathToIndexMap(ImmutableArray<MetadataReference> metadataReferences)
                {
                    var builder = ImmutableDictionary.CreateBuilder<string, HashSet<int>>(PathUtilities.Comparer);
 
                    for (var index = 0; index < metadataReferences.Length; index++)
                    {
                        var filePath = GetFilePath(metadataReferences[index]);
                        if (filePath != null)
                        {
                            builder.MultiAdd(filePath, index);
                        }
                    }
 
                    return builder.ToImmutable();
                }
 
                private static string? GetFilePath(MetadataReference metadataReference)
                {
                    return metadataReference switch
                    {
                        PortableExecutableReference portableExecutableReference => portableExecutableReference.FilePath,
                        UnresolvedMetadataReference unresolvedMetadataReference => unresolvedMetadataReference.Reference,
                        _ => null,
                    };
                }
 
                public void AddProjectReference(ProjectReference projectReference)
                {
                    _projectReferences.Add(projectReference);
                }
 
                public void SwapMetadataReferenceForProjectReference(ProjectReference projectReference, params string?[] possibleMetadataReferencePaths)
                {
                    foreach (var path in possibleMetadataReferencePaths)
                    {
                        if (path != null)
                        {
                            Remove(path);
                        }
                    }
 
                    AddProjectReference(projectReference);
                }
 
                /// <summary>
                /// Returns true if a metadata reference with the given file path is contained within this list.
                /// </summary>
                public bool Contains(string? filePath)
                    => filePath != null
                    && _pathToIndicesMap.ContainsKey(filePath);
 
                /// <summary>
                /// Removes the metadata reference with the given file path from this list.
                /// </summary>
                public void Remove(string filePath)
                {
                    if (filePath != null && _pathToIndicesMap.TryGetValue(filePath, out var indices))
                    {
                        _indicesToRemove.AddRange(indices);
                    }
                }
 
                public ProjectInfo? SelectProjectInfoByOutput(IEnumerable<ProjectInfo> projectInfos)
                {
                    foreach (var projectInfo in projectInfos)
                    {
                        var outputFilePath = projectInfo.OutputFilePath;
                        var outputRefFilePath = projectInfo.OutputRefFilePath;
                        if (outputFilePath != null &&
                            outputRefFilePath != null &&
                            (Contains(outputFilePath) || Contains(outputRefFilePath)))
                        {
                            return projectInfo;
                        }
                    }
 
                    return null;
                }
 
                public ImmutableArray<UnresolvedMetadataReference> GetUnresolvedMetadataReferences()
                {
                    var builder = ImmutableArray.CreateBuilder<UnresolvedMetadataReference>();
 
                    foreach (var metadataReference in GetMetadataReferences())
                    {
                        if (metadataReference is UnresolvedMetadataReference unresolvedMetadataReference)
                        {
                            builder.Add(unresolvedMetadataReference);
                        }
                    }
 
                    return builder.ToImmutableAndClear();
                }
 
                private ImmutableArray<MetadataReference> GetMetadataReferences()
                {
                    var builder = ImmutableArray.CreateBuilder<MetadataReference>();
 
                    // used to eliminate duplicates
                    var _ = PooledHashSet<MetadataReference>.GetInstance(out var set);
 
                    for (var index = 0; index < _metadataReferences.Length; index++)
                    {
                        var reference = _metadataReferences[index];
                        if (!_indicesToRemove.Contains(index) && set.Add(reference))
                        {
                            builder.Add(reference);
                        }
                    }
 
                    return builder.ToImmutableAndClear();
                }
 
                private ImmutableHashSet<ProjectReference> GetProjectReferences()
                    => _projectReferences.ToImmutable();
 
                public ResolvedReferences ToResolvedReferences()
                    => new(GetProjectReferences(), GetMetadataReferences());
            }
 
            private async Task<ResolvedReferences> ResolveReferencesAsync(ProjectId id, ProjectFileInfo projectFileInfo, CommandLineArguments commandLineArgs, CancellationToken cancellationToken)
            {
                // First, gather all of the metadata references from the command-line arguments.
                var resolvedMetadataReferences = commandLineArgs.ResolveMetadataReferences(
                    new WorkspaceMetadataFileReferenceResolver(
                        metadataService: _solutionServices.GetRequiredService<IMetadataService>(),
                        pathResolver: new RelativePathResolver(commandLineArgs.ReferencePaths, commandLineArgs.BaseDirectory)));
 
                var builder = new ResolvedReferencesBuilder(resolvedMetadataReferences);
 
                var projectDirectory = Path.GetDirectoryName(projectFileInfo.FilePath);
                RoslynDebug.AssertNotNull(projectDirectory);
 
                // Next, iterate through all project references in the file and create project references.
                foreach (var projectFileReference in projectFileInfo.ProjectReferences)
                {
                    var aliases = projectFileReference.Aliases;
 
                    if (_pathResolver.TryGetAbsoluteProjectPath(projectFileReference.Path, baseDirectory: projectDirectory, _discoveredProjectOptions.OnPathFailure, out var projectReferencePath))
                    {
                        // The easiest case is to add a reference to a project we already know about.
                        if (TryAddReferenceToKnownProject(id, projectReferencePath, aliases, builder))
                        {
                            continue;
                        }
 
                        if (projectFileReference.ReferenceOutputAssembly)
                        {
                            // If we don't know how to load a project (that is, it's not a language we support), we can still
                            // attempt to verify that its output exists on disk and is included in our set of metadata references.
                            // If it is, we'll just leave it in place.
                            if (!IsProjectLoadable(projectReferencePath) &&
                                await VerifyUnloadableProjectOutputExistsAsync(projectReferencePath, builder, cancellationToken).ConfigureAwait(false))
                            {
                                continue;
                            }
 
                            // If metadata is preferred, see if the project reference's output exists on disk and is included
                            // in our metadata references. If it is, don't create a project reference; we'll just use the metadata.
                            if (_preferMetadataForReferencesOfDiscoveredProjects &&
                                await VerifyProjectOutputExistsAsync(projectReferencePath, builder, cancellationToken).ConfigureAwait(false))
                            {
                                continue;
                            }
 
                            // Finally, we'll try to load and reference the project.
                            if (await TryLoadAndAddReferenceAsync(id, projectReferencePath, aliases, builder, cancellationToken).ConfigureAwait(false))
                            {
                                continue;
                            }
                        }
                        else
                        {
                            // Load the project but do not add a reference:
                            _ = await LoadProjectInfosFromPathAsync(projectReferencePath, _discoveredProjectOptions, cancellationToken).ConfigureAwait(false);
                            continue;
                        }
                    }
 
                    // We weren't able to handle this project reference, so add it without further processing.
                    var unknownProjectId = _projectMap.GetOrCreateProjectId(projectFileReference.Path);
                    var newProjectReference = CreateProjectReference(from: id, to: unknownProjectId, aliases);
                    builder.AddProjectReference(newProjectReference);
                }
 
                // Are there still any unresolved metadata references? If so, remove them and report diagnostics.
                foreach (var unresolvedMetadataReference in builder.GetUnresolvedMetadataReferences())
                {
                    var filePath = unresolvedMetadataReference.Reference;
 
                    builder.Remove(filePath);
 
                    _diagnosticReporter.Report(new ProjectDiagnostic(
                        WorkspaceDiagnosticKind.Warning,
                        string.Format(WorkspaceMSBuildResources.Unresolved_metadata_reference_removed_from_project_0, filePath),
                        id));
                }
 
                return builder.ToResolvedReferences();
            }
 
            private async Task<bool> TryLoadAndAddReferenceAsync(ProjectId id, string projectReferencePath, ImmutableArray<string> aliases, ResolvedReferencesBuilder builder, CancellationToken cancellationToken)
            {
                var projectReferenceInfos = await LoadProjectInfosFromPathAsync(projectReferencePath, _discoveredProjectOptions, cancellationToken).ConfigureAwait(false);
 
                if (projectReferenceInfos.IsEmpty)
                {
                    return false;
                }
 
                // Find the project reference info whose output we have a metadata reference for.
                ProjectInfo? projectReferenceInfo = null;
                foreach (var info in projectReferenceInfos)
                {
                    var outputFilePath = info.OutputFilePath;
                    var outputRefFilePath = info.OutputRefFilePath;
                    if (outputFilePath != null &&
                        outputRefFilePath != null &&
                        (builder.Contains(outputFilePath) || builder.Contains(outputRefFilePath)))
                    {
                        projectReferenceInfo = info;
                        break;
                    }
                }
 
                if (projectReferenceInfo is null)
                {
                    // We didn't find the project reference info that matches any of our metadata references.
                    // In this case, we'll go ahead and use the first project reference info that was found,
                    // but report a warning because this likely means that either a metadata reference path
                    // or a project output path is incorrect.
 
                    projectReferenceInfo = projectReferenceInfos[0];
 
                    _diagnosticReporter.Report(new ProjectDiagnostic(
                        WorkspaceDiagnosticKind.Warning,
                        string.Format(WorkspaceMSBuildResources.Found_project_reference_without_a_matching_metadata_reference_0, projectReferencePath),
                        id));
                }
 
                if (!ProjectReferenceExists(to: id, from: projectReferenceInfo))
                {
                    var newProjectReference = CreateProjectReference(from: id, to: projectReferenceInfo.Id, aliases);
                    builder.SwapMetadataReferenceForProjectReference(newProjectReference, projectReferenceInfo.OutputRefFilePath, projectReferenceInfo.OutputFilePath);
                }
                else
                {
                    // This project already has a reference on us. Don't introduce a circularity by referencing it.
                    // However, if the project's output doesn't exist on disk, we need to remove from our list of
                    // metadata references to avoid failures later. Essentially, the concern here is that the metadata
                    // reference is an UnresolvedMetadataReference, which will throw when we try to create a
                    // Compilation with it.
 
                    var outputRefFilePath = projectReferenceInfo.OutputRefFilePath;
                    if (outputRefFilePath != null && !File.Exists(outputRefFilePath))
                    {
                        builder.Remove(outputRefFilePath);
                    }
 
                    var outputFilePath = projectReferenceInfo.OutputFilePath;
                    if (outputFilePath != null && !File.Exists(outputFilePath))
                    {
                        builder.Remove(outputFilePath);
                    }
                }
 
                // Note that we return true even if we don't actually add a reference due to a circularity because,
                // in that case, we've still handled everything.
                return true;
            }
 
            private bool IsProjectLoadable(string projectPath)
                => _projectFileExtensionRegistry.TryGetLanguageNameFromProjectPath(projectPath, DiagnosticReportingMode.Ignore, out _);
 
            private async Task<bool> VerifyUnloadableProjectOutputExistsAsync(string projectPath, ResolvedReferencesBuilder builder, CancellationToken cancellationToken)
            {
                var buildHost = await _buildHostProcessManager.GetBuildHostWithFallbackAsync(projectPath, cancellationToken).ConfigureAwait(false);
                var outputFilePath = await buildHost.TryGetProjectOutputPathAsync(projectPath, cancellationToken).ConfigureAwait(false);
                return outputFilePath != null
                    && builder.Contains(outputFilePath)
                    && File.Exists(outputFilePath);
            }
 
            private async Task<bool> VerifyProjectOutputExistsAsync(string projectPath, ResolvedReferencesBuilder builder, CancellationToken cancellationToken)
            {
                // Note: Load the project, but don't report failures.
                var projectFileInfos = await LoadProjectFileInfosAsync(projectPath, DiagnosticReportingOptions.IgnoreAll, cancellationToken).ConfigureAwait(false);
 
                foreach (var projectFileInfo in projectFileInfos)
                {
                    var outputFilePath = projectFileInfo.OutputFilePath;
                    var outputRefFilePath = projectFileInfo.OutputRefFilePath;
 
                    if ((builder.Contains(outputFilePath) && File.Exists(outputFilePath)) ||
                        (builder.Contains(outputRefFilePath) && File.Exists(outputRefFilePath)))
                    {
                        return true;
                    }
                }
 
                return false;
            }
 
            private ProjectReference CreateProjectReference(ProjectId from, ProjectId to, ImmutableArray<string> aliases)
            {
                var newReference = new ProjectReference(to, aliases);
                _projectIdToProjectReferencesMap.MultiAdd(from, newReference);
                return newReference;
            }
 
            private bool ProjectReferenceExists(ProjectId to, ProjectId from)
                => _projectIdToProjectReferencesMap.TryGetValue(from, out var references)
                && references.Contains(pr => pr.ProjectId == to);
 
            private static bool ProjectReferenceExists(ProjectId to, ProjectInfo from)
                => from.ProjectReferences.Any(pr => pr.ProjectId == to);
 
            private bool TryAddReferenceToKnownProject(
                ProjectId id,
                string projectReferencePath,
                ImmutableArray<string> aliases,
                ResolvedReferencesBuilder builder)
            {
                if (_projectMap.TryGetIdsByProjectPath(projectReferencePath, out var projectReferenceIds))
                {
                    foreach (var projectReferenceId in projectReferenceIds)
                    {
                        // Don't add a reference if the project already has a reference on us. Otherwise, it will cause a circularity.
                        if (ProjectReferenceExists(to: id, from: projectReferenceId))
                        {
                            return false;
                        }
 
                        var outputRefFilePath = _projectMap.GetOutputRefFilePathById(projectReferenceId);
                        var outputFilePath = _projectMap.GetOutputFilePathById(projectReferenceId);
 
                        if (builder.Contains(outputRefFilePath) ||
                            builder.Contains(outputFilePath))
                        {
                            var newProjectReference = CreateProjectReference(from: id, to: projectReferenceId, aliases);
                            builder.SwapMetadataReferenceForProjectReference(newProjectReference, outputRefFilePath, outputFilePath);
                            return true;
                        }
                    }
                }
 
                return false;
            }
        }
    }
}