diff --git a/src/SharpIDE.Application/Features/Analysis/ProjectLoader/CustomMsBuildProjectLoader.Worker.cs b/src/SharpIDE.Application/Features/Analysis/ProjectLoader/CustomMsBuildProjectLoader.Worker.cs index 273cc15..efa5719 100644 --- a/src/SharpIDE.Application/Features/Analysis/ProjectLoader/CustomMsBuildProjectLoader.Worker.cs +++ b/src/SharpIDE.Application/Features/Analysis/ProjectLoader/CustomMsBuildProjectLoader.Worker.cs @@ -13,408 +13,408 @@ namespace SharpIDE.Application.Features.Analysis.ProjectLoader; public partial class CustomMsBuildProjectLoader { - private sealed partial class CustomWorker - { - private readonly SolutionServices _solutionServices; - private readonly DiagnosticReporter _diagnosticReporter; - private readonly PathResolver _pathResolver; - private readonly ProjectFileExtensionRegistry _projectFileExtensionRegistry; - private readonly BuildHostProcessManager _buildHostProcessManager; - private readonly string _baseDirectory; - - /// - /// 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. - /// - private readonly ImmutableArray _requestedProjectPaths; - - /// - /// Map of s, project paths, and output file paths. - /// - private readonly ProjectMap _projectMap; - - /// - /// Progress reporter. - /// - private readonly IProgress? _progress; - - /// - /// Provides options for how failures should be reported when loading requested project files. - /// - private readonly DiagnosticReportingOptions _requestedProjectOptions; - - /// - /// Provides options for how failures should be reported when loading any discovered project files. - /// - private readonly DiagnosticReportingOptions _discoveredProjectOptions; - - /// - /// When true, metadata is preferred for any project reference unless the referenced project is already loaded - /// because it was requested. - /// - private readonly bool _preferMetadataForReferencesOfDiscoveredProjects; - private readonly Dictionary _projectIdToFileInfoMap; - private readonly Dictionary> _projectIdToProjectReferencesMap; - private readonly Dictionary> _pathToDiscoveredProjectInfosMap; - - public CustomWorker( - SolutionServices services, - DiagnosticReporter diagnosticReporter, - PathResolver pathResolver, - ProjectFileExtensionRegistry projectFileExtensionRegistry, - BuildHostProcessManager buildHostProcessManager, - ImmutableArray requestedProjectPaths, - string baseDirectory, - ProjectMap? projectMap, - IProgress? progress, - DiagnosticReportingOptions requestedProjectOptions, - DiagnosticReportingOptions discoveredProjectOptions, - bool preferMetadataForReferencesOfDiscoveredProjects) - { - _solutionServices = services; - _diagnosticReporter = diagnosticReporter; - _pathResolver = pathResolver; - _projectFileExtensionRegistry = projectFileExtensionRegistry; - _buildHostProcessManager = buildHostProcessManager; - _baseDirectory = baseDirectory; - _requestedProjectPaths = requestedProjectPaths; - _projectMap = projectMap ?? ProjectMap.Create(); - _progress = progress; - _requestedProjectOptions = requestedProjectOptions; - _discoveredProjectOptions = discoveredProjectOptions; - _preferMetadataForReferencesOfDiscoveredProjects = preferMetadataForReferencesOfDiscoveredProjects; - _projectIdToFileInfoMap = []; - _pathToDiscoveredProjectInfosMap = new Dictionary>(PathUtilities.Comparer); - _projectIdToProjectReferencesMap = []; - } - - private async Task DoOperationAndReportProgressAsync(ProjectLoadOperation operation, string? projectPath, string? targetFramework, Func> 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, Dictionary)> LoadAsync(CancellationToken cancellationToken) - { - var results = ImmutableArray.CreateBuilder(); - var processedPaths = new HashSet(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(), _projectIdToFileInfoMap); - } - - private async Task> 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(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> 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(); - - 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 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(language) - ?.GetDefaultParseOptions(); - var compilationOptions = GetLanguageService(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(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(); - 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 ResolveAnalyzerReferences(CommandLineArguments commandLineArgs) - { - // The one line that is changed from the original Roslyn code: - var analyzerAssemblyLoaderProvider = GetWorkspaceService(); - if (analyzerAssemblyLoaderProvider is null) - { - var message = string.Format(WorkspaceMSBuildResources.Unable_to_find_0, nameof(IAnalyzerService)); - throw new Exception(message); - } - - var analyzerLoader = analyzerAssemblyLoaderProvider.SharedShadowCopyLoader; - - 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); - } - } - } + private sealed partial class CustomWorker + { + private readonly SolutionServices _solutionServices; + private readonly DiagnosticReporter _diagnosticReporter; + private readonly PathResolver _pathResolver; + private readonly ProjectFileExtensionRegistry _projectFileExtensionRegistry; + private readonly BuildHostProcessManager _buildHostProcessManager; + private readonly string _baseDirectory; + + /// + /// 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. + /// + private readonly ImmutableArray _requestedProjectPaths; + + /// + /// Map of s, project paths, and output file paths. + /// + private readonly ProjectMap _projectMap; + + /// + /// Progress reporter. + /// + private readonly IProgress? _progress; + + /// + /// Provides options for how failures should be reported when loading requested project files. + /// + private readonly DiagnosticReportingOptions _requestedProjectOptions; + + /// + /// Provides options for how failures should be reported when loading any discovered project files. + /// + private readonly DiagnosticReportingOptions _discoveredProjectOptions; + + /// + /// When true, metadata is preferred for any project reference unless the referenced project is already loaded + /// because it was requested. + /// + private readonly bool _preferMetadataForReferencesOfDiscoveredProjects; + private readonly Dictionary _projectIdToFileInfoMap; + private readonly Dictionary> _projectIdToProjectReferencesMap; + private readonly Dictionary> _pathToDiscoveredProjectInfosMap; + + public CustomWorker( + SolutionServices services, + DiagnosticReporter diagnosticReporter, + PathResolver pathResolver, + ProjectFileExtensionRegistry projectFileExtensionRegistry, + BuildHostProcessManager buildHostProcessManager, + ImmutableArray requestedProjectPaths, + string baseDirectory, + ProjectMap? projectMap, + IProgress? progress, + DiagnosticReportingOptions requestedProjectOptions, + DiagnosticReportingOptions discoveredProjectOptions, + bool preferMetadataForReferencesOfDiscoveredProjects) + { + _solutionServices = services; + _diagnosticReporter = diagnosticReporter; + _pathResolver = pathResolver; + _projectFileExtensionRegistry = projectFileExtensionRegistry; + _buildHostProcessManager = buildHostProcessManager; + _baseDirectory = baseDirectory; + _requestedProjectPaths = requestedProjectPaths; + _projectMap = projectMap ?? ProjectMap.Create(); + _progress = progress; + _requestedProjectOptions = requestedProjectOptions; + _discoveredProjectOptions = discoveredProjectOptions; + _preferMetadataForReferencesOfDiscoveredProjects = preferMetadataForReferencesOfDiscoveredProjects; + _projectIdToFileInfoMap = []; + _pathToDiscoveredProjectInfosMap = new Dictionary>(PathUtilities.Comparer); + _projectIdToProjectReferencesMap = []; + } + + private async Task DoOperationAndReportProgressAsync(ProjectLoadOperation operation, string? projectPath, string? targetFramework, Func> 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, Dictionary)> LoadAsync(CancellationToken cancellationToken) + { + var results = ImmutableArray.CreateBuilder(); + var processedPaths = new HashSet(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(), _projectIdToFileInfoMap); + } + + private async Task> 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(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> 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(); + + 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 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(language) + ?.GetDefaultParseOptions(); + var compilationOptions = GetLanguageService(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(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(); + 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 ResolveAnalyzerReferences(CommandLineArguments commandLineArgs) + { + // The one line that is changed from the original Roslyn code: + var analyzerAssemblyLoaderProvider = GetWorkspaceService(); + if (analyzerAssemblyLoaderProvider is null) + { + var message = string.Format(WorkspaceMSBuildResources.Unable_to_find_0, nameof(IAnalyzerService)); + throw new Exception(message); + } + + var analyzerLoader = analyzerAssemblyLoaderProvider.SharedShadowCopyLoader; + + 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); + } + } + } var analyzerReferences = commandLineArgs.ResolveAnalyzerReferences(analyzerLoader).Distinct(Microsoft.CodeAnalysis.MSBuild.MSBuildProjectLoader.Worker.AnalyzerReferencePathComparer.Instance); var isolatedReferences = IsolatedAnalyzerReferenceSet.CreateIsolatedAnalyzerReferencesAsync( @@ -424,80 +424,80 @@ public partial class CustomMsBuildProjectLoader CancellationToken.None).VerifyCompleted(); return isolatedReferences; - } + } - private ImmutableArray CreateDocumentInfos(IReadOnlyList documentFileInfos, ProjectId projectId, Encoding? encoding) - { - var results = new FixedSizeArrayBuilder(documentFileInfos.Count); + private ImmutableArray CreateDocumentInfos(IReadOnlyList documentFileInfos, ProjectId projectId, Encoding? encoding) + { + var results = new FixedSizeArrayBuilder(documentFileInfos.Count); - foreach (var info in documentFileInfos) - { - GetDocumentNameAndFolders(info.LogicalPath, out var name, out var folders); + 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); + 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); - } + results.Add(documentInfo); + } - return results.MoveToImmutable(); - } + return results.MoveToImmutable(); + } - private static readonly char[] s_directorySplitChars = [Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar]; + private static readonly char[] s_directorySplitChars = [Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar]; - private static void GetDocumentNameAndFolders(string logicalPath, out string name, out ImmutableArray folders) - { - var pathNames = logicalPath.Split(s_directorySplitChars, StringSplitOptions.RemoveEmptyEntries); - if (pathNames.Length > 0) - { - folders = pathNames.Length > 1 - ? [.. pathNames.Take(pathNames.Length - 1)] - : []; + private static void GetDocumentNameAndFolders(string logicalPath, out string name, out ImmutableArray folders) + { + var pathNames = logicalPath.Split(s_directorySplitChars, StringSplitOptions.RemoveEmptyEntries); + if (pathNames.Length > 0) + { + folders = pathNames.Length > 1 + ? [.. pathNames.Take(pathNames.Length - 1)] + : []; - name = pathNames[^1]; - } - else - { - name = logicalPath; - folders = []; - } - } + name = pathNames[^1]; + } + else + { + name = logicalPath; + folders = []; + } + } - private void CheckForDuplicateDocuments(ImmutableArray documents, string? projectFilePath, ProjectId projectId) - { - var paths = new HashSet(PathUtilities.Comparer); - foreach (var doc in documents) - { - if (doc.FilePath is null) - continue; + private void CheckForDuplicateDocuments(ImmutableArray documents, string? projectFilePath, ProjectId projectId) + { + var paths = new HashSet(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); + 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); - } + _diagnosticReporter.Report(diagnostic); + } - paths.Add(doc.FilePath); - } - } + paths.Add(doc.FilePath); + } + } - private TLanguageService? GetLanguageService(string languageName) - where TLanguageService : ILanguageService - => _solutionServices - .GetLanguageServices(languageName) - .GetService(); + private TLanguageService? GetLanguageService(string languageName) + where TLanguageService : ILanguageService + => _solutionServices + .GetLanguageServices(languageName) + .GetService(); - private TWorkspaceService? GetWorkspaceService() - where TWorkspaceService : IWorkspaceService - => _solutionServices - .GetService(); - } + private TWorkspaceService? GetWorkspaceService() + where TWorkspaceService : IWorkspaceService + => _solutionServices + .GetService(); + } } diff --git a/src/SharpIDE.Application/Features/Analysis/ProjectLoader/CustomMsBuildProjectLoader.Worker_ResolveReferences.cs b/src/SharpIDE.Application/Features/Analysis/ProjectLoader/CustomMsBuildProjectLoader.Worker_ResolveReferences.cs index 84e8f05..dc6a0d8 100644 --- a/src/SharpIDE.Application/Features/Analysis/ProjectLoader/CustomMsBuildProjectLoader.Worker_ResolveReferences.cs +++ b/src/SharpIDE.Application/Features/Analysis/ProjectLoader/CustomMsBuildProjectLoader.Worker_ResolveReferences.cs @@ -9,400 +9,400 @@ namespace SharpIDE.Application.Features.Analysis.ProjectLoader; public partial class CustomMsBuildProjectLoader { - private sealed partial class CustomWorker - { - private readonly struct ResolvedReferences - { - public ImmutableHashSet ProjectReferences { get; } - public ImmutableArray MetadataReferences { get; } + private sealed partial class CustomWorker + { + private readonly struct ResolvedReferences + { + public ImmutableHashSet ProjectReferences { get; } + public ImmutableArray MetadataReferences { get; } - public ResolvedReferences(ImmutableHashSet projectReferences, ImmutableArray metadataReferences) - { - ProjectReferences = projectReferences; - MetadataReferences = metadataReferences; - } - } + public ResolvedReferences(ImmutableHashSet projectReferences, ImmutableArray metadataReferences) + { + ProjectReferences = projectReferences; + MetadataReferences = metadataReferences; + } + } - /// - /// 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. - /// - private sealed class ResolvedReferencesBuilder - { - /// - /// The full list of s. - /// - private readonly ImmutableArray _metadataReferences; + /// + /// 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. + /// + private sealed class ResolvedReferencesBuilder + { + /// + /// The full list of s. + /// + private readonly ImmutableArray _metadataReferences; - /// - /// 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. - /// - private readonly ImmutableDictionary> _pathToIndicesMap; + /// + /// 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. + /// + private readonly ImmutableDictionary> _pathToIndicesMap; - /// - /// A set of indices into that are to be removed. - /// - private readonly HashSet _indicesToRemove; + /// + /// A set of indices into that are to be removed. + /// + private readonly HashSet _indicesToRemove; - private readonly ImmutableHashSet.Builder _projectReferences; + private readonly ImmutableHashSet.Builder _projectReferences; - public ResolvedReferencesBuilder(IEnumerable metadataReferences) - { - _metadataReferences = [.. metadataReferences]; - _pathToIndicesMap = CreatePathToIndexMap(_metadataReferences); - _indicesToRemove = []; - _projectReferences = ImmutableHashSet.CreateBuilder(); - } + public ResolvedReferencesBuilder(IEnumerable metadataReferences) + { + _metadataReferences = [.. metadataReferences]; + _pathToIndicesMap = CreatePathToIndexMap(_metadataReferences); + _indicesToRemove = []; + _projectReferences = ImmutableHashSet.CreateBuilder(); + } - private static ImmutableDictionary> CreatePathToIndexMap(ImmutableArray metadataReferences) - { - var builder = ImmutableDictionary.CreateBuilder>(PathUtilities.Comparer); + private static ImmutableDictionary> CreatePathToIndexMap(ImmutableArray metadataReferences) + { + var builder = ImmutableDictionary.CreateBuilder>(PathUtilities.Comparer); - for (var index = 0; index < metadataReferences.Length; index++) - { - var filePath = GetFilePath(metadataReferences[index]); - if (filePath != null) - { - builder.MultiAdd(filePath, index); - } - } + for (var index = 0; index < metadataReferences.Length; index++) + { + var filePath = GetFilePath(metadataReferences[index]); + if (filePath != null) + { + builder.MultiAdd(filePath, index); + } + } - return builder.ToImmutable(); - } + return builder.ToImmutable(); + } - private static string? GetFilePath(MetadataReference metadataReference) - { - return metadataReference switch - { - PortableExecutableReference portableExecutableReference => portableExecutableReference.FilePath, - UnresolvedMetadataReference unresolvedMetadataReference => unresolvedMetadataReference.Reference, - _ => null, - }; - } + 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 AddProjectReference(ProjectReference projectReference) + { + _projectReferences.Add(projectReference); + } - public void SwapMetadataReferenceForProjectReference(ProjectReference projectReference, params string?[] possibleMetadataReferencePaths) - { - foreach (var path in possibleMetadataReferencePaths) - { - if (path != null) - { - Remove(path); - } - } + public void SwapMetadataReferenceForProjectReference(ProjectReference projectReference, params string?[] possibleMetadataReferencePaths) + { + foreach (var path in possibleMetadataReferencePaths) + { + if (path != null) + { + Remove(path); + } + } - AddProjectReference(projectReference); - } + AddProjectReference(projectReference); + } - /// - /// Returns true if a metadata reference with the given file path is contained within this list. - /// - public bool Contains(string? filePath) - => filePath != null - && _pathToIndicesMap.ContainsKey(filePath); + /// + /// Returns true if a metadata reference with the given file path is contained within this list. + /// + public bool Contains(string? filePath) + => filePath != null + && _pathToIndicesMap.ContainsKey(filePath); - /// - /// Removes the metadata reference with the given file path from this list. - /// - public void Remove(string filePath) - { - if (filePath != null && _pathToIndicesMap.TryGetValue(filePath, out var indices)) - { - _indicesToRemove.AddRange(indices); - } - } + /// + /// Removes the metadata reference with the given file path from this list. + /// + public void Remove(string filePath) + { + if (filePath != null && _pathToIndicesMap.TryGetValue(filePath, out var indices)) + { + _indicesToRemove.AddRange(indices); + } + } - public ProjectInfo? SelectProjectInfoByOutput(IEnumerable 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; - } - } + public ProjectInfo? SelectProjectInfoByOutput(IEnumerable 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; - } + return null; + } - public ImmutableArray GetUnresolvedMetadataReferences() - { - var builder = ImmutableArray.CreateBuilder(); + public ImmutableArray GetUnresolvedMetadataReferences() + { + var builder = ImmutableArray.CreateBuilder(); - foreach (var metadataReference in GetMetadataReferences()) - { - if (metadataReference is UnresolvedMetadataReference unresolvedMetadataReference) - { - builder.Add(unresolvedMetadataReference); - } - } + foreach (var metadataReference in GetMetadataReferences()) + { + if (metadataReference is UnresolvedMetadataReference unresolvedMetadataReference) + { + builder.Add(unresolvedMetadataReference); + } + } - return builder.ToImmutableAndClear(); - } + return builder.ToImmutableAndClear(); + } - private ImmutableArray GetMetadataReferences() - { - var builder = ImmutableArray.CreateBuilder(); + private ImmutableArray GetMetadataReferences() + { + var builder = ImmutableArray.CreateBuilder(); - // used to eliminate duplicates - var _ = PooledHashSet.GetInstance(out var set); + // used to eliminate duplicates + var _ = PooledHashSet.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); - } - } + 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(); - } + return builder.ToImmutableAndClear(); + } - private ImmutableHashSet GetProjectReferences() - => _projectReferences.ToImmutable(); + private ImmutableHashSet GetProjectReferences() + => _projectReferences.ToImmutable(); - public ResolvedReferences ToResolvedReferences() - => new(GetProjectReferences(), GetMetadataReferences()); - } + public ResolvedReferences ToResolvedReferences() + => new(GetProjectReferences(), GetMetadataReferences()); + } - private async Task 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(), - pathResolver: new RelativePathResolver(commandLineArgs.ReferencePaths, commandLineArgs.BaseDirectory))); + private async Task 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(), + pathResolver: new RelativePathResolver(commandLineArgs.ReferencePaths, commandLineArgs.BaseDirectory))); - var builder = new ResolvedReferencesBuilder(resolvedMetadataReferences); + var builder = new ResolvedReferencesBuilder(resolvedMetadataReferences); - var projectDirectory = Path.GetDirectoryName(projectFileInfo.FilePath); - RoslynDebug.AssertNotNull(projectDirectory); + 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; + // 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 (_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 (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; - } + // 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; - } - } + // 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); - } + // 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; + // 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); + builder.Remove(filePath); - _diagnosticReporter.Report(new ProjectDiagnostic( - WorkspaceDiagnosticKind.Warning, - string.Format(WorkspaceMSBuildResources.Unresolved_metadata_reference_removed_from_project_0, filePath), - id)); - } + _diagnosticReporter.Report(new ProjectDiagnostic( + WorkspaceDiagnosticKind.Warning, + string.Format(WorkspaceMSBuildResources.Unresolved_metadata_reference_removed_from_project_0, filePath), + id)); + } - return builder.ToResolvedReferences(); - } + return builder.ToResolvedReferences(); + } - private async Task TryLoadAndAddReferenceAsync(ProjectId id, string projectReferencePath, ImmutableArray aliases, ResolvedReferencesBuilder builder, CancellationToken cancellationToken) - { - var projectReferenceInfos = await LoadProjectInfosFromPathAsync(projectReferencePath, _discoveredProjectOptions, cancellationToken).ConfigureAwait(false); + private async Task TryLoadAndAddReferenceAsync(ProjectId id, string projectReferencePath, ImmutableArray aliases, ResolvedReferencesBuilder builder, CancellationToken cancellationToken) + { + var projectReferenceInfos = await LoadProjectInfosFromPathAsync(projectReferencePath, _discoveredProjectOptions, cancellationToken).ConfigureAwait(false); - if (projectReferenceInfos.IsEmpty) - { - return 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; - } - } + // 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. + 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]; + projectReferenceInfo = projectReferenceInfos[0]; - _diagnosticReporter.Report(new ProjectDiagnostic( - WorkspaceDiagnosticKind.Warning, - string.Format(WorkspaceMSBuildResources.Found_project_reference_without_a_matching_metadata_reference_0, projectReferencePath), - id)); - } + _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. + 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 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); - } - } + 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; - } + // 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 bool IsProjectLoadable(string projectPath) + => _projectFileExtensionRegistry.TryGetLanguageNameFromProjectPath(projectPath, DiagnosticReportingMode.Ignore, out _); - private async Task 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 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 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); + private async Task 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; + 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; - } - } + if ((builder.Contains(outputFilePath) && File.Exists(outputFilePath)) || + (builder.Contains(outputRefFilePath) && File.Exists(outputRefFilePath))) + { + return true; + } + } - return false; - } + return false; + } - private ProjectReference CreateProjectReference(ProjectId from, ProjectId to, ImmutableArray aliases) - { - var newReference = new ProjectReference(to, aliases); - _projectIdToProjectReferencesMap.MultiAdd(from, newReference); - return newReference; - } + private ProjectReference CreateProjectReference(ProjectId from, ProjectId to, ImmutableArray 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.Any(pr => pr.ProjectId == to); + private bool ProjectReferenceExists(ProjectId to, ProjectId from) + => _projectIdToProjectReferencesMap.TryGetValue(from, out var references) + && references.Any(pr => pr.ProjectId == to); - private static bool ProjectReferenceExists(ProjectId to, ProjectInfo from) - => from.ProjectReferences.Any(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 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; - } + private bool TryAddReferenceToKnownProject( + ProjectId id, + string projectReferencePath, + ImmutableArray 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); + 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; - } - } - } + if (builder.Contains(outputRefFilePath) || + builder.Contains(outputFilePath)) + { + var newProjectReference = CreateProjectReference(from: id, to: projectReferenceId, aliases); + builder.SwapMetadataReferenceForProjectReference(newProjectReference, outputRefFilePath, outputFilePath); + return true; + } + } + } - return false; - } - } + return false; + } + } } diff --git a/src/SharpIDE.Application/Features/Analysis/ProjectLoader/CustomMsBuildProjectLoader.cs b/src/SharpIDE.Application/Features/Analysis/ProjectLoader/CustomMsBuildProjectLoader.cs index 0b22dc4..8dadef8 100644 --- a/src/SharpIDE.Application/Features/Analysis/ProjectLoader/CustomMsBuildProjectLoader.cs +++ b/src/SharpIDE.Application/Features/Analysis/ProjectLoader/CustomMsBuildProjectLoader.cs @@ -56,68 +56,68 @@ public partial class CustomMsBuildProjectLoader(Workspace workspace, ImmutableDi } /// - /// Loads the for the specified solution file, including all projects referenced by the solution file and - /// all the projects referenced by the project files. - /// - /// The path to the solution file to be loaded. This may be an absolute path or a path relative to the - /// current working directory. - /// An optional that will receive updates as the solution is loaded. - /// An optional that will log MSBuild results. - /// An optional to allow cancellation of this operation. - public new async Task<(SolutionInfo, Dictionary)> LoadSolutionInfoAsync( - string solutionFilePath, - IProgress? progress = null, - ILogger? msbuildLogger = null, - CancellationToken cancellationToken = default) - { - if (solutionFilePath == null) - { - throw new ArgumentNullException(nameof(solutionFilePath)); - } + /// Loads the for the specified solution file, including all projects referenced by the solution file and + /// all the projects referenced by the project files. + /// + /// The path to the solution file to be loaded. This may be an absolute path or a path relative to the + /// current working directory. + /// An optional that will receive updates as the solution is loaded. + /// An optional that will log MSBuild results. + /// An optional to allow cancellation of this operation. + public new async Task<(SolutionInfo, Dictionary)> LoadSolutionInfoAsync( + string solutionFilePath, + IProgress? progress = null, + ILogger? msbuildLogger = null, + CancellationToken cancellationToken = default) + { + if (solutionFilePath == null) + { + throw new ArgumentNullException(nameof(solutionFilePath)); + } - var reportingMode = GetReportingModeForUnrecognizedProjects(); + var reportingMode = GetReportingModeForUnrecognizedProjects(); - var reportingOptions = new DiagnosticReportingOptions( - onPathFailure: reportingMode, - onLoaderFailure: reportingMode); + var reportingOptions = new DiagnosticReportingOptions( + onPathFailure: reportingMode, + onLoaderFailure: reportingMode); - var (absoluteSolutionPath, projects) = await SolutionFileReader.ReadSolutionFileAsync(solutionFilePath, _pathResolver, reportingMode, cancellationToken).ConfigureAwait(false); - var projectPaths = projects.SelectAsArray(p => p.ProjectPath); + var (absoluteSolutionPath, projects) = await SolutionFileReader.ReadSolutionFileAsync(solutionFilePath, _pathResolver, reportingMode, cancellationToken).ConfigureAwait(false); + var projectPaths = projects.SelectAsArray(p => p.ProjectPath); - using (_dataGuard.DisposableWait(cancellationToken)) - { - SetSolutionProperties(absoluteSolutionPath); - } + using (_dataGuard.DisposableWait(cancellationToken)) + { + SetSolutionProperties(absoluteSolutionPath); + } - IBinLogPathProvider binLogPathProvider = null!; // TODO: Fix + IBinLogPathProvider binLogPathProvider = null!; // TODO: Fix - var buildHostProcessManager = new BuildHostProcessManager(Properties, binLogPathProvider, _loggerFactory); - await using var _ = buildHostProcessManager.ConfigureAwait(false); + var buildHostProcessManager = new BuildHostProcessManager(Properties, binLogPathProvider, _loggerFactory); + await using var _ = buildHostProcessManager.ConfigureAwait(false); - var worker = new CustomWorker( - _solutionServices, - _diagnosticReporter, - _pathResolver, - _projectFileExtensionRegistry, - buildHostProcessManager, - projectPaths, - // TryGetAbsoluteSolutionPath should not return an invalid path - baseDirectory: Path.GetDirectoryName(absoluteSolutionPath)!, - projectMap: null, - progress, - requestedProjectOptions: reportingOptions, - discoveredProjectOptions: reportingOptions, - preferMetadataForReferencesOfDiscoveredProjects: false); + var worker = new CustomWorker( + _solutionServices, + _diagnosticReporter, + _pathResolver, + _projectFileExtensionRegistry, + buildHostProcessManager, + projectPaths, + // TryGetAbsoluteSolutionPath should not return an invalid path + baseDirectory: Path.GetDirectoryName(absoluteSolutionPath)!, + projectMap: null, + progress, + requestedProjectOptions: reportingOptions, + discoveredProjectOptions: reportingOptions, + preferMetadataForReferencesOfDiscoveredProjects: false); - var (projectInfos, projectFileInfos) = await worker.LoadAsync(cancellationToken).ConfigureAwait(false); + var (projectInfos, projectFileInfos) = await worker.LoadAsync(cancellationToken).ConfigureAwait(false); - // construct workspace from loaded project infos - var solutionInfo = SolutionInfo.Create( - SolutionId.CreateNewId(debugName: absoluteSolutionPath), - version: default, - absoluteSolutionPath, - projectInfos); + // construct workspace from loaded project infos + var solutionInfo = SolutionInfo.Create( + SolutionId.CreateNewId(debugName: absoluteSolutionPath), + version: default, + absoluteSolutionPath, + projectInfos); - return (solutionInfo, projectFileInfos); - } + return (solutionInfo, projectFileInfos); + } }