Shadow copy analyzer assemblies

This commit is contained in:
Matt Parker
2025-11-20 00:45:35 +10:00
parent 348aed4ae5
commit f7c2f7fdfc
6 changed files with 1052 additions and 51 deletions

View File

@@ -1,51 +0,0 @@
using System.Collections.Immutable;
using Microsoft.Build.Framework;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.MSBuild;
namespace SharpIDE.Application.Features.Analysis;
public class CustomMsBuildProjectLoader(Workspace workspace, ImmutableDictionary<string, string>? properties = null) : MSBuildProjectLoader(workspace, properties)
{
public async Task<ImmutableArray<ProjectInfo>> LoadProjectInfosAsync(
List<string> projectFilePaths,
ProjectMap? projectMap = null,
IProgress<ProjectLoadProgress>? progress = null,
#pragma warning disable IDE0060 // TODO: decide what to do with this unusued ILogger, since we can't reliabily use it if we're sending builds out of proc
ILogger? msbuildLogger = null,
#pragma warning restore IDE0060
CancellationToken cancellationToken = default)
{
if (projectFilePaths.Count is 0)
{
throw new ArgumentException("At least one project file path must be specified.", nameof(projectFilePaths));
}
var requestedProjectOptions = DiagnosticReportingOptions.ThrowForAll;
var reportingMode = GetReportingModeForUnrecognizedProjects();
var discoveredProjectOptions = new DiagnosticReportingOptions(
onPathFailure: reportingMode,
onLoaderFailure: reportingMode);
var buildHostProcessManager = new BuildHostProcessManager(Properties, loggerFactory: _loggerFactory);
await using var _ = buildHostProcessManager.ConfigureAwait(false);
var worker = new Worker(
_solutionServices,
_diagnosticReporter,
_pathResolver,
_projectFileExtensionRegistry,
buildHostProcessManager,
requestedProjectPaths: projectFilePaths.ToImmutableArray(),
baseDirectory: Directory.GetCurrentDirectory(),
projectMap,
progress,
requestedProjectOptions,
discoveredProjectOptions,
this.LoadMetadataForReferencedProjects);
return await worker.LoadAsync(cancellationToken).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,26 @@
// 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 Microsoft.CodeAnalysis.Diagnostics;
namespace SharpIDE.Application.Features.Analysis.ProjectLoader;
public partial class CustomMsBuildProjectLoader
{
private sealed partial class CustomWorker
{
private sealed class AnalyzerReferencePathComparer : IEqualityComparer<AnalyzerReference?>
{
public static AnalyzerReferencePathComparer Instance = new();
private AnalyzerReferencePathComparer() { }
public bool Equals(AnalyzerReference? x, AnalyzerReference? y)
=> string.Equals(x?.FullPath, y?.FullPath, StringComparison.OrdinalIgnoreCase);
public int GetHashCode(AnalyzerReference? obj)
=> obj?.FullPath?.GetHashCode() ?? 0;
}
}
}

View File

@@ -0,0 +1,496 @@
using System.Collections.Immutable;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.MSBuild;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
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;
/// <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>
/// 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 CustomWorker(
SolutionServices services,
DiagnosticReporter diagnosticReporter,
PathResolver pathResolver,
ProjectFileExtensionRegistry projectFileExtensionRegistry,
BuildHostProcessManager buildHostProcessManager,
ImmutableArray<string> requestedProjectPaths,
string baseDirectory,
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;
_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)
{
// The one line that is changed from the original Roslyn code:
var analyzerAssemblyLoaderProvider = GetWorkspaceService<IAnalyzerAssemblyLoaderProvider>();
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);
}
}
}
return commandLineArgs.ResolveAnalyzerReferences(analyzerLoader).Distinct(Microsoft.CodeAnalysis.MSBuild.MSBuildProjectLoader.Worker.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)]
: [];
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>();
}
}

View File

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

View File

@@ -0,0 +1,121 @@
using System.Collections.Immutable;
using Microsoft.Build.Framework;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.MSBuild;
namespace SharpIDE.Application.Features.Analysis.ProjectLoader;
// I really don't like having to duplicate this, but we need to use IAnalyzerAssemblyLoaderProvider rather than IAnalyzerService,
// so that analyzers are shadow copied to prevent locking.
// My attempts to provide a custom IAnalyzerService to the MEF composition were in vain.
// I think this will only be temporary, as I think a more sophisticated ProjectLoader mechanism is going to be necessary.
// see roslyn LanguageServerProjectLoader, LanguageServerProjectSystem, ProjectSystemProjectFactory
// https://github.com/dotnet/roslyn/blob/main/src/Workspaces/MSBuild/Core/MSBuild/MSBuildProjectLoader.cs
public partial class CustomMsBuildProjectLoader(Workspace workspace, ImmutableDictionary<string, string>? properties = null) : MSBuildProjectLoader(workspace, properties)
{
public async Task<ImmutableArray<ProjectInfo>> LoadProjectInfosAsync(
List<string> projectFilePaths,
ProjectMap? projectMap = null,
IProgress<ProjectLoadProgress>? progress = null,
#pragma warning disable IDE0060 // TODO: decide what to do with this unusued ILogger, since we can't reliabily use it if we're sending builds out of proc
ILogger? msbuildLogger = null,
#pragma warning restore IDE0060
CancellationToken cancellationToken = default)
{
if (projectFilePaths.Count is 0)
{
throw new ArgumentException("At least one project file path must be specified.", nameof(projectFilePaths));
}
var requestedProjectOptions = DiagnosticReportingOptions.ThrowForAll;
var reportingMode = GetReportingModeForUnrecognizedProjects();
var discoveredProjectOptions = new DiagnosticReportingOptions(
onPathFailure: reportingMode,
onLoaderFailure: reportingMode);
var buildHostProcessManager = new BuildHostProcessManager(Properties, loggerFactory: _loggerFactory);
await using var _ = buildHostProcessManager.ConfigureAwait(false);
var worker = new CustomWorker(
_solutionServices,
_diagnosticReporter,
_pathResolver,
_projectFileExtensionRegistry,
buildHostProcessManager,
requestedProjectPaths: projectFilePaths.ToImmutableArray(),
baseDirectory: Directory.GetCurrentDirectory(),
projectMap,
progress,
requestedProjectOptions,
discoveredProjectOptions,
this.LoadMetadataForReferencedProjects);
return await worker.LoadAsync(cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Loads the <see cref="SolutionInfo"/> for the specified solution file, including all projects referenced by the solution file and
/// all the projects referenced by the project files.
/// </summary>
/// <param name="solutionFilePath">The path to the solution file to be loaded. This may be an absolute path or a path relative to the
/// current working directory.</param>
/// <param name="progress">An optional <see cref="IProgress{T}"/> that will receive updates as the solution is loaded.</param>
/// <param name="msbuildLogger">An optional <see cref="ILogger"/> that will log MSBuild results.</param>
/// <param name="cancellationToken">An optional <see cref="CancellationToken"/> to allow cancellation of this operation.</param>
public new async Task<SolutionInfo> LoadSolutionInfoAsync(
string solutionFilePath,
IProgress<ProjectLoadProgress>? progress = null,
ILogger? msbuildLogger = null,
CancellationToken cancellationToken = default)
{
if (solutionFilePath == null)
{
throw new ArgumentNullException(nameof(solutionFilePath));
}
var reportingMode = GetReportingModeForUnrecognizedProjects();
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);
using (_dataGuard.DisposableWait(cancellationToken))
{
SetSolutionProperties(absoluteSolutionPath);
}
IBinLogPathProvider binLogPathProvider = null!; // TODO: Fix
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 projectInfos = await worker.LoadAsync(cancellationToken).ConfigureAwait(false);
// construct workspace from loaded project infos
return SolutionInfo.Create(
SolutionId.CreateNewId(debugName: absoluteSolutionPath),
version: default,
absoluteSolutionPath,
projectInfos);
}
}

View File

@@ -26,6 +26,7 @@ using Microsoft.Extensions.Logging;
using NuGet.Frameworks;
using Roslyn.LanguageServer.Protocol;
using SharpIDE.Application.Features.Analysis.FixLoaders;
using SharpIDE.Application.Features.Analysis.ProjectLoader;
using SharpIDE.Application.Features.Analysis.Razor;
using SharpIDE.Application.Features.Build;
using SharpIDE.Application.Features.SolutionDiscovery;