convert spaces to tabs
This commit is contained in:
@@ -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;
|
||||
|
||||
/// <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>, Dictionary<ProjectId, ProjectFileInfo>)> 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(), _projectIdToFileInfoMap);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
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>, Dictionary<ProjectId, ProjectFileInfo>)> 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(), _projectIdToFileInfoMap);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
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<DocumentInfo> CreateDocumentInfos(IReadOnlyList<DocumentFileInfo> documentFileInfos, ProjectId projectId, Encoding? encoding)
|
||||
{
|
||||
var results = new FixedSizeArrayBuilder<DocumentInfo>(documentFileInfos.Count);
|
||||
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);
|
||||
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<string> 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<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 = [];
|
||||
}
|
||||
}
|
||||
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;
|
||||
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);
|
||||
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<TLanguageService>(string languageName)
|
||||
where TLanguageService : ILanguageService
|
||||
=> _solutionServices
|
||||
.GetLanguageServices(languageName)
|
||||
.GetService<TLanguageService>();
|
||||
private TLanguageService? GetLanguageService<TLanguageService>(string languageName)
|
||||
where TLanguageService : ILanguageService
|
||||
=> _solutionServices
|
||||
.GetLanguageServices(languageName)
|
||||
.GetService<TLanguageService>();
|
||||
|
||||
private TWorkspaceService? GetWorkspaceService<TWorkspaceService>()
|
||||
where TWorkspaceService : IWorkspaceService
|
||||
=> _solutionServices
|
||||
.GetService<TWorkspaceService>();
|
||||
}
|
||||
private TWorkspaceService? GetWorkspaceService<TWorkspaceService>()
|
||||
where TWorkspaceService : IWorkspaceService
|
||||
=> _solutionServices
|
||||
.GetService<TWorkspaceService>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ProjectReference> ProjectReferences { get; }
|
||||
public ImmutableArray<MetadataReference> MetadataReferences { get; }
|
||||
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;
|
||||
}
|
||||
}
|
||||
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>
|
||||
/// 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 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;
|
||||
/// <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;
|
||||
private readonly ImmutableHashSet<ProjectReference>.Builder _projectReferences;
|
||||
|
||||
public ResolvedReferencesBuilder(IEnumerable<MetadataReference> metadataReferences)
|
||||
{
|
||||
_metadataReferences = [.. metadataReferences];
|
||||
_pathToIndicesMap = CreatePathToIndexMap(_metadataReferences);
|
||||
_indicesToRemove = [];
|
||||
_projectReferences = ImmutableHashSet.CreateBuilder<ProjectReference>();
|
||||
}
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// 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);
|
||||
}
|
||||
}
|
||||
/// <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;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public ImmutableArray<UnresolvedMetadataReference> GetUnresolvedMetadataReferences()
|
||||
{
|
||||
var builder = ImmutableArray.CreateBuilder<UnresolvedMetadataReference>();
|
||||
public ImmutableArray<UnresolvedMetadataReference> GetUnresolvedMetadataReferences()
|
||||
{
|
||||
var builder = ImmutableArray.CreateBuilder<UnresolvedMetadataReference>();
|
||||
|
||||
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<MetadataReference> GetMetadataReferences()
|
||||
{
|
||||
var builder = ImmutableArray.CreateBuilder<MetadataReference>();
|
||||
private ImmutableArray<MetadataReference> GetMetadataReferences()
|
||||
{
|
||||
var builder = ImmutableArray.CreateBuilder<MetadataReference>();
|
||||
|
||||
// used to eliminate duplicates
|
||||
var _ = PooledHashSet<MetadataReference>.GetInstance(out var set);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
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<ProjectReference> GetProjectReferences()
|
||||
=> _projectReferences.ToImmutable();
|
||||
private ImmutableHashSet<ProjectReference> GetProjectReferences()
|
||||
=> _projectReferences.ToImmutable();
|
||||
|
||||
public ResolvedReferences ToResolvedReferences()
|
||||
=> new(GetProjectReferences(), GetMetadataReferences());
|
||||
}
|
||||
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)));
|
||||
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 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<bool> TryLoadAndAddReferenceAsync(ProjectId id, string projectReferencePath, ImmutableArray<string> aliases, ResolvedReferencesBuilder builder, CancellationToken cancellationToken)
|
||||
{
|
||||
var projectReferenceInfos = await LoadProjectInfosFromPathAsync(projectReferencePath, _discoveredProjectOptions, cancellationToken).ConfigureAwait(false);
|
||||
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;
|
||||
}
|
||||
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<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> 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);
|
||||
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;
|
||||
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<string> aliases)
|
||||
{
|
||||
var newReference = new ProjectReference(to, aliases);
|
||||
_projectIdToProjectReferencesMap.MultiAdd(from, newReference);
|
||||
return newReference;
|
||||
}
|
||||
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 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<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;
|
||||
}
|
||||
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);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,68 +56,68 @@ public partial class CustomMsBuildProjectLoader(Workspace workspace, ImmutableDi
|
||||
}
|
||||
|
||||
/// <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, Dictionary<ProjectId, ProjectFileInfo>)> LoadSolutionInfoAsync(
|
||||
string solutionFilePath,
|
||||
IProgress<ProjectLoadProgress>? progress = null,
|
||||
ILogger? msbuildLogger = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (solutionFilePath == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(solutionFilePath));
|
||||
}
|
||||
/// 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, Dictionary<ProjectId, ProjectFileInfo>)> 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 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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user