File: MSBuild\MSBuildProjectLoader.Worker.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;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.MSBuild
{
    public partial class MSBuildProjectLoader
    {
        private partial class Worker
        {
            private readonly SolutionServices _solutionServices;
            private readonly DiagnosticReporter _diagnosticReporter;
            private readonly PathResolver _pathResolver;
            private readonly ProjectFileExtensionRegistry _projectFileExtensionRegistry;
            private readonly BuildHostProcessManager _buildHostProcessManager;
            private readonly string _baseDirectory;
 
            /// <summary>
            /// An ordered list of paths to project files that should be loaded. In the case of a solution,
            /// this is the list of project file paths in the solution.
            /// </summary>
            private readonly ImmutableArray<string> _requestedProjectPaths;
 
            /// <summary>
            /// Global MSBuild properties to set when loading projects.
            /// </summary>
            private readonly ImmutableDictionary<string, string> _globalProperties;
 
            /// <summary>
            /// Map of <see cref="ProjectId"/>s, project paths, and output file paths.
            /// </summary>
            private readonly ProjectMap _projectMap;
 
            /// <summary>
            /// Progress reporter.
            /// </summary>
            private readonly IProgress<ProjectLoadProgress>? _progress;
 
            /// <summary>
            /// Provides options for how failures should be reported when loading requested project files.
            /// </summary>
            private readonly DiagnosticReportingOptions _requestedProjectOptions;
 
            /// <summary>
            /// Provides options for how failures should be reported when loading any discovered project files.
            /// </summary>
            private readonly DiagnosticReportingOptions _discoveredProjectOptions;
 
            /// <summary>
            /// When true, metadata is preferred for any project reference unless the referenced project is already loaded
            /// because it was requested.
            /// </summary>
            private readonly bool _preferMetadataForReferencesOfDiscoveredProjects;
            private readonly Dictionary<ProjectId, ProjectFileInfo> _projectIdToFileInfoMap;
            private readonly Dictionary<ProjectId, List<ProjectReference>> _projectIdToProjectReferencesMap;
            private readonly Dictionary<string, ImmutableArray<ProjectInfo>> _pathToDiscoveredProjectInfosMap;
 
            public Worker(
                SolutionServices services,
                DiagnosticReporter diagnosticReporter,
                PathResolver pathResolver,
                ProjectFileExtensionRegistry projectFileExtensionRegistry,
                BuildHostProcessManager buildHostProcessManager,
                ImmutableArray<string> requestedProjectPaths,
                string baseDirectory,
                ImmutableDictionary<string, string> globalProperties,
                ProjectMap? projectMap,
                IProgress<ProjectLoadProgress>? progress,
                DiagnosticReportingOptions requestedProjectOptions,
                DiagnosticReportingOptions discoveredProjectOptions,
                bool preferMetadataForReferencesOfDiscoveredProjects)
            {
                _solutionServices = services;
                _diagnosticReporter = diagnosticReporter;
                _pathResolver = pathResolver;
                _projectFileExtensionRegistry = projectFileExtensionRegistry;
                _buildHostProcessManager = buildHostProcessManager;
                _baseDirectory = baseDirectory;
                _requestedProjectPaths = requestedProjectPaths;
                _globalProperties = globalProperties;
                _projectMap = projectMap ?? ProjectMap.Create();
                _progress = progress;
                _requestedProjectOptions = requestedProjectOptions;
                _discoveredProjectOptions = discoveredProjectOptions;
                _preferMetadataForReferencesOfDiscoveredProjects = preferMetadataForReferencesOfDiscoveredProjects;
                _projectIdToFileInfoMap = [];
                _pathToDiscoveredProjectInfosMap = new Dictionary<string, ImmutableArray<ProjectInfo>>(PathUtilities.Comparer);
                _projectIdToProjectReferencesMap = [];
            }
 
            private async Task<TResult> DoOperationAndReportProgressAsync<TResult>(ProjectLoadOperation operation, string? projectPath, string? targetFramework, Func<Task<TResult>> doFunc)
            {
                var watch = _progress != null
                    ? Stopwatch.StartNew()
                    : null;
 
                TResult result;
                try
                {
                    result = await doFunc().ConfigureAwait(false);
                }
                finally
                {
                    if (_progress != null && watch != null)
                    {
                        watch.Stop();
                        _progress.Report(new ProjectLoadProgress(projectPath ?? string.Empty, operation, targetFramework, watch.Elapsed));
                    }
                }
 
                return result;
            }
 
            public async Task<ImmutableArray<ProjectInfo>> LoadAsync(CancellationToken cancellationToken)
            {
                var results = ImmutableArray.CreateBuilder<ProjectInfo>();
                var processedPaths = new HashSet<string>(PathUtilities.Comparer);
 
                foreach (var projectPath in _requestedProjectPaths)
                {
                    cancellationToken.ThrowIfCancellationRequested();
 
                    if (!_pathResolver.TryGetAbsoluteProjectPath(projectPath, _baseDirectory, _requestedProjectOptions.OnPathFailure, out var absoluteProjectPath))
                    {
                        continue; // Failure should already be reported.
                    }
 
                    if (!processedPaths.Add(absoluteProjectPath))
                    {
                        _diagnosticReporter.Report(
                            new WorkspaceDiagnostic(
                                WorkspaceDiagnosticKind.Warning,
                                string.Format(WorkspaceMSBuildResources.Duplicate_project_discovered_and_skipped_0, absoluteProjectPath)));
 
                        continue;
                    }
 
                    var projectFileInfos = await LoadProjectInfosFromPathAsync(absoluteProjectPath, _requestedProjectOptions, cancellationToken).ConfigureAwait(false);
 
                    results.AddRange(projectFileInfos);
                }
 
                foreach (var (projectPath, projectInfos) in _pathToDiscoveredProjectInfosMap)
                {
                    cancellationToken.ThrowIfCancellationRequested();
 
                    if (!processedPaths.Contains(projectPath))
                    {
                        results.AddRange(projectInfos);
                    }
                }
 
                return results.ToImmutableAndClear();
            }
 
            private async Task<ImmutableArray<ProjectFileInfo>> LoadProjectFileInfosAsync(string projectPath, DiagnosticReportingOptions reportingOptions, CancellationToken cancellationToken)
            {
                if (!_projectFileExtensionRegistry.TryGetLanguageNameFromProjectPath(projectPath, reportingOptions.OnLoaderFailure, out var languageName))
                {
                    return []; // Failure should already be reported.
                }
 
                var preferredBuildHostKind = BuildHostProcessManager.GetKindForProject(projectPath);
                var (buildHost, actualBuildHostKind) = await _buildHostProcessManager.GetBuildHostWithFallbackAsync(preferredBuildHostKind, projectPath, cancellationToken).ConfigureAwait(false);
                var projectFile = await DoOperationAndReportProgressAsync(
                    ProjectLoadOperation.Evaluate,
                    projectPath,
                    targetFramework: null,
                    () => buildHost.LoadProjectFileAsync(projectPath, languageName, cancellationToken)
                ).ConfigureAwait(false);
 
                // If there were any failures during load, we won't be able to build the project. So, bail early with an empty project.
                var diagnosticItems = await projectFile.GetDiagnosticLogItemsAsync(cancellationToken).ConfigureAwait(false);
                if (diagnosticItems.Any(d => d.Kind == DiagnosticLogItemKind.Error))
                {
                    _diagnosticReporter.Report(diagnosticItems);
 
                    return [ProjectFileInfo.CreateEmpty(languageName, projectPath)];
                }
 
                var projectFileInfos = await DoOperationAndReportProgressAsync(
                    ProjectLoadOperation.Build,
                    projectPath,
                    targetFramework: null,
                    () => projectFile.GetProjectFileInfosAsync(cancellationToken)
                ).ConfigureAwait(false);
 
                var results = ImmutableArray.CreateBuilder<ProjectFileInfo>(projectFileInfos.Length);
 
                foreach (var projectFileInfo in projectFileInfos)
                {
                    // Note: any diagnostics would have been logged to the original project file's log.
 
                    results.Add(projectFileInfo);
                }
 
                // We'll go check for any further diagnostics and report them
                diagnosticItems = await projectFile.GetDiagnosticLogItemsAsync(cancellationToken).ConfigureAwait(false);
                _diagnosticReporter.Report(diagnosticItems);
 
                return results.MoveToImmutable();
            }
 
            private async Task<ImmutableArray<ProjectInfo>> LoadProjectInfosFromPathAsync(
                string projectPath, DiagnosticReportingOptions reportingOptions, CancellationToken cancellationToken)
            {
                if (_projectMap.TryGetProjectInfosByProjectPath(projectPath, out var results) ||
                    _pathToDiscoveredProjectInfosMap.TryGetValue(projectPath, out results))
                {
                    return results;
                }
 
                var builder = ImmutableArray.CreateBuilder<ProjectInfo>();
 
                var projectFileInfos = await LoadProjectFileInfosAsync(projectPath, reportingOptions, cancellationToken).ConfigureAwait(false);
 
                var idsAndFileInfos = new List<(ProjectId id, ProjectFileInfo fileInfo)>();
 
                foreach (var projectFileInfo in projectFileInfos)
                {
                    var projectId = _projectMap.GetOrCreateProjectId(projectFileInfo);
 
                    if (_projectIdToFileInfoMap.ContainsKey(projectId))
                    {
                        // There are multiple projects with the same project path and output path. This can happen
                        // if a multi-TFM project does not have unique output file paths for each TFM. In that case,
                        // we'll create a new ProjectId to ensure that the project is added to the workspace.
 
                        _diagnosticReporter.Report(
                            DiagnosticReportingMode.Log,
                            string.Format(WorkspaceMSBuildResources.Found_project_with_the_same_file_path_and_output_path_as_another_project_0, projectFileInfo.FilePath));
 
                        projectId = ProjectId.CreateNewId(debugName: projectFileInfo.FilePath);
                    }
 
                    idsAndFileInfos.Add((projectId, projectFileInfo));
                    _projectIdToFileInfoMap.Add(projectId, projectFileInfo);
                }
 
                // If this project resulted in more than a single project, a discriminator (e.g. TFM) should be
                // added to the project name.
                var addDiscriminator = idsAndFileInfos.Count > 1;
 
                foreach (var (id, fileInfo) in idsAndFileInfos)
                {
                    var projectInfo = await CreateProjectInfoAsync(fileInfo, id, addDiscriminator, cancellationToken).ConfigureAwait(false);
 
                    builder.Add(projectInfo);
                    _projectMap.AddProjectInfo(projectInfo);
                }
 
                results = builder.ToImmutable();
 
                _pathToDiscoveredProjectInfosMap.Add(projectPath, results);
 
                return results;
            }
 
            private Task<ProjectInfo> CreateProjectInfoAsync(ProjectFileInfo projectFileInfo, ProjectId projectId, bool addDiscriminator, CancellationToken cancellationToken)
            {
                var language = projectFileInfo.Language;
                var projectPath = projectFileInfo.FilePath;
                var projectName = Path.GetFileNameWithoutExtension(projectPath) ?? string.Empty;
                if (addDiscriminator && !RoslynString.IsNullOrWhiteSpace(projectFileInfo.TargetFramework))
                {
                    projectName += "(" + projectFileInfo.TargetFramework + ")";
                }
 
                var version = projectPath is null
                    ? VersionStamp.Default
                    : VersionStamp.Create(FileUtilities.GetFileTimeStamp(projectPath));
 
                if (projectFileInfo.IsEmpty)
                {
                    var assemblyName = GetAssemblyNameFromProjectPath(projectPath);
 
                    var parseOptions = GetLanguageService<ISyntaxTreeFactoryService>(language)
                        ?.GetDefaultParseOptions();
                    var compilationOptions = GetLanguageService<ICompilationFactoryService>(language)
                        ?.GetDefaultCompilationOptions();
 
                    return Task.FromResult(
                        ProjectInfo.Create(
                            new ProjectInfo.ProjectAttributes(
                                projectId,
                                version,
                                name: projectName,
                                assemblyName: assemblyName,
                                language: language,
                                compilationOutputInfo: new CompilationOutputInfo(projectFileInfo.IntermediateOutputFilePath, projectFileInfo.GeneratedFilesOutputDirectory),
                                checksumAlgorithm: SourceHashAlgorithms.Default,
                                outputFilePath: projectFileInfo.OutputFilePath,
                                outputRefFilePath: projectFileInfo.OutputRefFilePath,
                                filePath: projectPath),
                            compilationOptions: compilationOptions,
                            parseOptions: parseOptions));
                }
 
                return DoOperationAndReportProgressAsync(ProjectLoadOperation.Resolve, projectPath, projectFileInfo.TargetFramework, async () =>
                {
                    var projectDirectory = Path.GetDirectoryName(projectPath);
 
                    // parse command line arguments
                    var commandLineParser = GetLanguageService<ICommandLineParserService>(projectFileInfo.Language);
 
                    if (commandLineParser is null)
                    {
                        var message = string.Format(WorkspaceMSBuildResources.Unable_to_find_a_0_for_1, nameof(ICommandLineParserService), projectFileInfo.Language);
                        throw new Exception(message);
                    }
 
                    var commandLineArgs = commandLineParser.Parse(
                        arguments: projectFileInfo.CommandLineArgs,
                        baseDirectory: projectDirectory,
                        isInteractive: false,
                        sdkDirectory: RuntimeEnvironment.GetRuntimeDirectory());
 
                    var assemblyName = commandLineArgs.CompilationName;
                    if (RoslynString.IsNullOrWhiteSpace(assemblyName))
                    {
                        // if there isn't an assembly name, make one from the file path.
                        // Note: This may not be necessary any longer if the command line args
                        // always produce a valid compilation name.
                        assemblyName = GetAssemblyNameFromProjectPath(projectPath);
                    }
 
                    // Ensure sure that doc-comments are parsed
                    var parseOptions = commandLineArgs.ParseOptions;
                    if (parseOptions.DocumentationMode == DocumentationMode.None)
                    {
                        parseOptions = parseOptions.WithDocumentationMode(DocumentationMode.Parse);
                    }
 
                    // add all the extra options that are really behavior overrides
                    var metadataService = GetWorkspaceService<IMetadataService>();
                    var compilationOptions = commandLineArgs.CompilationOptions
                        .WithXmlReferenceResolver(new XmlFileResolver(projectDirectory))
                        .WithSourceReferenceResolver(new SourceFileResolver([], projectDirectory))
                        // TODO: https://github.com/dotnet/roslyn/issues/4967
                        .WithMetadataReferenceResolver(new WorkspaceMetadataFileReferenceResolver(metadataService, new RelativePathResolver([], projectDirectory)))
                        .WithStrongNameProvider(new DesktopStrongNameProvider(commandLineArgs.KeyFileSearchPaths, Path.GetTempPath()))
                        .WithAssemblyIdentityComparer(DesktopAssemblyIdentityComparer.Default);
 
                    var documents = CreateDocumentInfos(projectFileInfo.Documents, projectId, commandLineArgs.Encoding);
                    var additionalDocuments = CreateDocumentInfos(projectFileInfo.AdditionalDocuments, projectId, commandLineArgs.Encoding);
                    var analyzerConfigDocuments = CreateDocumentInfos(projectFileInfo.AnalyzerConfigDocuments, projectId, commandLineArgs.Encoding);
                    CheckForDuplicateDocuments(documents.Concat(additionalDocuments).Concat(analyzerConfigDocuments), projectPath, projectId);
 
                    var analyzerReferences = ResolveAnalyzerReferences(commandLineArgs);
 
                    var resolvedReferences = await ResolveReferencesAsync(projectId, projectFileInfo, commandLineArgs, cancellationToken).ConfigureAwait(false);
 
                    return ProjectInfo.Create(
                        new ProjectInfo.ProjectAttributes(
                            projectId,
                            version,
                            projectName,
                            assemblyName,
                            language,
                            compilationOutputInfo: new CompilationOutputInfo(projectFileInfo.IntermediateOutputFilePath, projectFileInfo.GeneratedFilesOutputDirectory),
                            checksumAlgorithm: commandLineArgs.ChecksumAlgorithm,
                            filePath: projectPath,
                            outputFilePath: projectFileInfo.OutputFilePath,
                            outputRefFilePath: projectFileInfo.OutputRefFilePath,
                            isSubmission: false),
                        compilationOptions: compilationOptions,
                        parseOptions: parseOptions,
                        documents: documents,
                        projectReferences: resolvedReferences.ProjectReferences,
                        metadataReferences: resolvedReferences.MetadataReferences,
                        analyzerReferences: analyzerReferences,
                        additionalDocuments: additionalDocuments,
                        hostObjectType: null)
                        .WithDefaultNamespace(projectFileInfo.DefaultNamespace)
                        .WithAnalyzerConfigDocuments(analyzerConfigDocuments);
                });
            }
 
            private static string GetAssemblyNameFromProjectPath(string? projectFilePath)
            {
                var assemblyName = Path.GetFileNameWithoutExtension(projectFilePath);
 
                // if this is still unreasonable, use a fixed name.
                if (RoslynString.IsNullOrWhiteSpace(assemblyName))
                {
                    assemblyName = "assembly";
                }
 
                return assemblyName;
            }
 
            private IEnumerable<AnalyzerReference> ResolveAnalyzerReferences(CommandLineArguments commandLineArgs)
            {
                var analyzerService = GetWorkspaceService<IAnalyzerService>();
                if (analyzerService is null)
                {
                    var message = string.Format(WorkspaceMSBuildResources.Unable_to_find_0, nameof(IAnalyzerService));
                    throw new Exception(message);
                }
 
                var analyzerLoader = analyzerService.GetLoader();
 
                foreach (var path in commandLineArgs.AnalyzerReferences.Select(r => r.FilePath))
                {
                    string? fullPath;
 
                    if (PathUtilities.IsAbsolute(path))
                    {
                        fullPath = FileUtilities.TryNormalizeAbsolutePath(path);
 
                        if (fullPath != null && File.Exists(fullPath))
                        {
                            analyzerLoader.AddDependencyLocation(fullPath);
                        }
                    }
                }
 
                return commandLineArgs.ResolveAnalyzerReferences(analyzerLoader).Distinct(AnalyzerReferencePathComparer.Instance);
            }
 
            private ImmutableArray<DocumentInfo> CreateDocumentInfos(IReadOnlyList<DocumentFileInfo> documentFileInfos, ProjectId projectId, Encoding? encoding)
            {
                var results = new FixedSizeArrayBuilder<DocumentInfo>(documentFileInfos.Count);
 
                foreach (var info in documentFileInfos)
                {
                    GetDocumentNameAndFolders(info.LogicalPath, out var name, out var folders);
 
                    var documentInfo = DocumentInfo.Create(
                        DocumentId.CreateNewId(projectId, debugName: info.FilePath),
                        name,
                        folders,
                        SourceCodeKind.Regular,
                        new WorkspaceFileTextLoader(_solutionServices, info.FilePath, encoding),
                        info.FilePath,
                        info.IsGenerated);
 
                    results.Add(documentInfo);
                }
 
                return results.MoveToImmutable();
            }
 
            private static readonly char[] s_directorySplitChars = [Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar];
 
            private static void GetDocumentNameAndFolders(string logicalPath, out string name, out ImmutableArray<string> folders)
            {
                var pathNames = logicalPath.Split(s_directorySplitChars, StringSplitOptions.RemoveEmptyEntries);
                if (pathNames.Length > 0)
                {
                    folders = pathNames.Length > 1
                        ? pathNames.Take(pathNames.Length - 1).ToImmutableArray()
                        : [];
 
                    name = pathNames[^1];
                }
                else
                {
                    name = logicalPath;
                    folders = [];
                }
            }
 
            private void CheckForDuplicateDocuments(ImmutableArray<DocumentInfo> documents, string? projectFilePath, ProjectId projectId)
            {
                var paths = new HashSet<string>(PathUtilities.Comparer);
                foreach (var doc in documents)
                {
                    if (doc.FilePath is null)
                        continue;
 
                    if (paths.Contains(doc.FilePath))
                    {
                        var message = string.Format(WorkspacesResources.Duplicate_source_file_0_in_project_1, doc.FilePath, projectFilePath);
                        var diagnostic = new ProjectDiagnostic(WorkspaceDiagnosticKind.Warning, message, projectId);
 
                        _diagnosticReporter.Report(diagnostic);
                    }
 
                    paths.Add(doc.FilePath);
                }
            }
 
            private TLanguageService? GetLanguageService<TLanguageService>(string languageName)
                where TLanguageService : ILanguageService
                => _solutionServices
                    .GetLanguageServices(languageName)
                    .GetService<TLanguageService>();
 
            private TWorkspaceService? GetWorkspaceService<TWorkspaceService>()
                where TWorkspaceService : IWorkspaceService
                => _solutionServices
                    .GetService<TWorkspaceService>();
        }
    }
}