|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.ProjectSystem;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.Workspaces.ProjectSystem;
internal sealed partial class ProjectSystemProjectFactory
{
/// <summary>
/// The main gate to synchronize updates to this solution.
/// </summary>
/// <remarks>
/// See the Readme.md in this directory for further comments about threading in this area.
/// </remarks>
// TODO: we should be able to get rid of this gate in favor of just calling the various workspace methods that acquire the Workspace's
// serialization lock and then allow us to update our own state under that lock.
private readonly SemaphoreSlim _gate = new(initialCount: 1);
/// <summary>
/// Stores the latest state of the project system factory.
/// Access to this is synchronized via <see cref="_gate"/>
/// </summary>
private ProjectUpdateState _projectUpdateState = ProjectUpdateState.Empty;
public Workspace Workspace { get; }
public IAsynchronousOperationListener WorkspaceListener { get; }
public IFileChangeWatcher FileChangeWatcher { get; }
public FileWatchedReferenceFactory<PortableExecutableReference> FileWatchedPortableExecutableReferenceFactory { get; }
public FileWatchedReferenceFactory<AnalyzerReference> FileWatchedAnalyzerReferenceFactory { get; }
public SolutionServices SolutionServices => this.Workspace.Services.SolutionServices;
private readonly Func<bool, ImmutableArray<string>, Task> _onDocumentsAddedMaybeAsync;
private readonly Action<Project> _onProjectRemoved;
/// <summary>
/// A set of documents that were added by <see cref="ProjectSystemProject.AddSourceTextContainer"/>, and aren't otherwise
/// tracked for opening/closing.
/// </summary>
public ImmutableHashSet<DocumentId> DocumentsNotFromFiles { get; private set; } = [];
/// <remarks>Should be updated with <see cref="ImmutableInterlocked"/>.</remarks>
private ImmutableDictionary<ProjectId, string?> _projectToMaxSupportedLangVersionMap = ImmutableDictionary<ProjectId, string?>.Empty;
/// <remarks>Should be updated with <see cref="ImmutableInterlocked"/>.</remarks>
private ImmutableDictionary<ProjectId, string> _projectToDependencyNodeTargetIdentifier = ImmutableDictionary<ProjectId, string>.Empty;
/// <summary>
/// Set by the host if the solution is currently closing; this can be used to optimize some things there.
/// </summary>
public bool SolutionClosing { get; set; }
/// <summary>
/// The current path to the solution. Currently this is only used to update the solution path when the first project is added -- we don't have a concept
/// of the solution path changing in the middle while a bunch of projects are loaded.
/// </summary>
public string? SolutionPath { get; set; }
public Guid SolutionTelemetryId { get; set; }
public ProjectSystemProjectFactory(
Workspace workspace,
IFileChangeWatcher fileChangeWatcher,
Func<bool, ImmutableArray<string>, Task> onDocumentsAddedMaybeAsync,
Action<Project> onProjectRemoved,
CancellationToken cancellationToken)
{
Workspace = workspace;
FileChangeWatcher = fileChangeWatcher;
_onDocumentsAddedMaybeAsync = onDocumentsAddedMaybeAsync;
_onProjectRemoved = onProjectRemoved;
WorkspaceListener = this.SolutionServices.GetRequiredService<IWorkspaceAsynchronousOperationListenerProvider>().GetListener();
FileWatchedPortableExecutableReferenceFactory = new(fileChangeWatcher, WorkspaceListener, this.StartRefreshingMetadataReferencesForFileAsync, cancellationToken);
FileWatchedAnalyzerReferenceFactory = new(fileChangeWatcher, WorkspaceListener, this.StartRefreshingAnalyzerReferenceForFileAsync, cancellationToken);
}
public FileTextLoader CreateFileTextLoader(string fullPath)
=> new WorkspaceFileTextLoader(this.SolutionServices, fullPath, defaultEncoding: null);
public async Task<ProjectSystemProject> CreateAndAddToWorkspaceAsync(string projectSystemName, string language, ProjectSystemProjectCreationInfo creationInfo, ProjectSystemHostInfo hostInfo)
{
var projectId = ProjectId.CreateNewId(projectSystemName);
var assemblyName = creationInfo.AssemblyName ?? projectSystemName;
// We will use the project system name as the default display name of the project
var project = new ProjectSystemProject(
this,
hostInfo,
projectId,
displayName: projectSystemName,
language,
assemblyName,
creationInfo.CompilationOptions,
creationInfo.FilePath,
creationInfo.ParseOptions);
var versionStamp = creationInfo.FilePath != null
? VersionStamp.Create(File.GetLastWriteTimeUtc(creationInfo.FilePath))
: VersionStamp.Create();
var projectInfo = ProjectInfo.Create(
new ProjectInfo.ProjectAttributes(
projectId,
versionStamp,
name: projectSystemName,
assemblyName,
language,
// generatedFilesOutputDirectory to be updated when initializing project from command line:
compilationOutputInfo: new(creationInfo.CompilationOutputAssemblyFilePath, generatedFilesOutputDirectory: null),
SourceHashAlgorithms.Default, // will be updated when command line is set
outputFilePath: creationInfo.CompilationOutputAssemblyFilePath,
filePath: creationInfo.FilePath,
telemetryId: creationInfo.TelemetryId,
hasSdkCodeStyleAnalyzers: project.HasSdkCodeStyleAnalyzers),
compilationOptions: creationInfo.CompilationOptions,
parseOptions: creationInfo.ParseOptions);
await ApplyChangeToWorkspaceAsync(w =>
{
// We call the synchronous SetCurrentSolution which is fine here since we've already acquired our outer lock so this will
// never block. But once we remove the ProjectSystemProjectFactory lock in favor of everybody calling the newer overloads of
// SetCurrentSolution, this should become async again.
w.SetCurrentSolution(
oldSolution =>
{
// If we don't have any projects and this is our first project being added, then we'll create a
// new SolutionId and count this as the solution being added so that event is raised.
if (oldSolution.ProjectIds.Count == 0)
{
var solutionInfo = SolutionInfo.Create(
SolutionId.CreateNewId(SolutionPath),
VersionStamp.Create(),
SolutionPath,
projects: [projectInfo],
analyzerReferences: w.CurrentSolution.AnalyzerReferences).WithTelemetryId(SolutionTelemetryId);
var newSolution = w.CreateSolution(solutionInfo);
using var _ = ArrayBuilder<ProjectInfo>.GetInstance(out var projectInfos);
projectInfos.AddRange(solutionInfo.Projects);
newSolution = newSolution.AddProjects(projectInfos);
return newSolution;
}
else
{
return oldSolution.AddProject(projectInfo);
}
},
(oldSolution, newSolution) =>
{
return oldSolution.ProjectIds.Count == 0
? (WorkspaceChangeKind.SolutionAdded, projectId: null, documentId: null)
: (WorkspaceChangeKind.ProjectAdded, projectId, documentId: null);
},
onBeforeUpdate: null,
onAfterUpdate: null);
}).ConfigureAwait(false);
// Set this value early after solution is created so it is available to Razor. This will get updated
// when the command line is set, but we want a non-null value to be available as soon as possible.
//
// Set the property in a batch; if we set the property directly we'll be taking a synchronous lock here and
// potentially block up thread pool threads. Doing this in a batch means the global lock will be acquired asynchronously.
var disposableBatchScope = await project.CreateBatchScopeAsync(CancellationToken.None).ConfigureAwait(false);
await using var _ = disposableBatchScope.ConfigureAwait(false);
project.CompilationOutputAssemblyFilePath = creationInfo.CompilationOutputAssemblyFilePath;
return project;
}
public string? TryGetDependencyNodeTargetIdentifier(ProjectId projectId)
{
// This doesn't take a lock since _projectToDependencyNodeTargetIdentifier is immutable
_projectToDependencyNodeTargetIdentifier.TryGetValue(projectId, out var identifier);
return identifier;
}
public string? TryGetMaxSupportedLanguageVersion(ProjectId projectId)
{
// This doesn't take a lock since _projectToMaxSupportedLangVersionMap is immutable
_projectToMaxSupportedLangVersionMap.TryGetValue(projectId, out var identifier);
return identifier;
}
internal void AddDocumentToDocumentsNotFromFiles_NoLock(DocumentId documentId)
{
Contract.ThrowIfFalse(_gate.CurrentCount == 0);
DocumentsNotFromFiles = DocumentsNotFromFiles.Add(documentId);
}
internal void RemoveDocumentToDocumentsNotFromFiles_NoLock(DocumentId documentId)
{
Contract.ThrowIfFalse(_gate.CurrentCount == 0);
DocumentsNotFromFiles = DocumentsNotFromFiles.Remove(documentId);
}
/// <summary>
/// Applies a single operation to the workspace. <paramref name="action"/> should be a call to one of the protected Workspace.On* methods.
/// </summary>
public void ApplyChangeToWorkspace(Action<Workspace> action)
{
using (_gate.DisposableWait())
{
action(Workspace);
}
}
/// <summary>
/// Applies a single operation to the workspace. <paramref name="action"/> should be a call to one of the protected Workspace.On* methods.
/// </summary>
public async ValueTask ApplyChangeToWorkspaceAsync(Action<Workspace> action, CancellationToken cancellationToken = default)
{
using (await _gate.DisposableWaitAsync(cancellationToken).ConfigureAwait(false))
{
action(Workspace);
}
}
/// <summary>
/// Applies a single operation to the workspace. <paramref name="action"/> should be a call to one of the protected Workspace.On* methods.
/// </summary>
public async ValueTask ApplyChangeToWorkspaceMaybeAsync(bool useAsync, Action<Workspace> action)
{
using (useAsync ? await _gate.DisposableWaitAsync().ConfigureAwait(false) : _gate.DisposableWait())
{
action(Workspace);
}
}
/// <summary>
/// Applies a single operation to the workspace that also needs to update the <see cref="_projectUpdateState"/>.
/// <paramref name="action"/> should be a call to one of the protected Workspace.On* methods.
/// </summary>
public void ApplyChangeToWorkspaceWithProjectUpdateState(Func<Workspace, ProjectUpdateState, ProjectUpdateState> action)
{
using (_gate.DisposableWait())
{
var projectUpdateState = action(Workspace, _projectUpdateState);
ApplyProjectUpdateState(projectUpdateState);
}
}
/// <summary>
/// Applies a solution transformation to the workspace and triggers workspace changed event for specified <paramref name="projectId"/>.
/// The transformation shall only update the project of the solution with the specified <paramref name="projectId"/>.
///
/// The <paramref name="solutionTransformation"/> function must be safe to be attempted multiple times (and not update local state).
/// </summary>
public void ApplyChangeToWorkspace(ProjectId projectId, Func<CodeAnalysis.Solution, CodeAnalysis.Solution> solutionTransformation)
{
using (_gate.DisposableWait())
{
Workspace.SetCurrentSolution(solutionTransformation, WorkspaceChangeKind.ProjectChanged, projectId);
}
}
/// <inheritdoc cref="ApplyBatchChangeToWorkspaceAsync(Func{SolutionChangeAccumulator, ProjectUpdateState, ProjectUpdateState}, Action{ProjectUpdateState}?)"/>
public void ApplyBatchChangeToWorkspace(Func<SolutionChangeAccumulator, ProjectUpdateState, ProjectUpdateState> mutation, Action<ProjectUpdateState>? onAfterUpdateAlways)
{
ApplyBatchChangeToWorkspaceMaybeAsync(useAsync: false, mutation, onAfterUpdateAlways).VerifyCompleted();
}
/// <inheritdoc cref="ApplyBatchChangeToWorkspaceAsync(Func{SolutionChangeAccumulator, ProjectUpdateState, ProjectUpdateState}, Action{ProjectUpdateState}?)"/>
public Task ApplyBatchChangeToWorkspaceAsync(Func<SolutionChangeAccumulator, ProjectUpdateState, ProjectUpdateState> mutation, Action<ProjectUpdateState>? onAfterUpdateAlways)
{
return ApplyBatchChangeToWorkspaceMaybeAsync(useAsync: true, mutation, onAfterUpdateAlways);
}
/// <inheritdoc cref="ApplyBatchChangeToWorkspaceAsync(Func{SolutionChangeAccumulator, ProjectUpdateState, ProjectUpdateState}, Action{ProjectUpdateState}?)"/>
public async Task ApplyBatchChangeToWorkspaceMaybeAsync(bool useAsync, Func<SolutionChangeAccumulator, ProjectUpdateState, ProjectUpdateState> mutation, Action<ProjectUpdateState>? onAfterUpdateAlways)
{
using (useAsync ? await _gate.DisposableWaitAsync().ConfigureAwait(false) : _gate.DisposableWait())
{
await ApplyBatchChangeToWorkspaceMaybe_NoLockAsync(useAsync, mutation, onAfterUpdateAlways).ConfigureAwait(false);
}
}
/// <summary>
/// Applies a change to the workspace that can do any number of project changes.
/// The mutation action must be safe to attempt multiple times, in case there are interceding solution changes.
/// If outside changes need to run under the global lock and run only once, they should use the <paramref name="onAfterUpdateAlways"/> action.
/// <paramref name="onAfterUpdateAlways"/> will always run even if the transformation applied no changes.
/// </summary>
/// <remarks>This is needed to synchronize with <see cref="ApplyChangeToWorkspace(Action{Workspace})" /> to avoid any races. This
/// method could be moved down to the core Workspace layer and then could use the synchronization lock there.</remarks>
public async Task ApplyBatchChangeToWorkspaceMaybe_NoLockAsync(bool useAsync, Func<SolutionChangeAccumulator, ProjectUpdateState, ProjectUpdateState> mutation, Action<ProjectUpdateState>? onAfterUpdateAlways)
{
Contract.ThrowIfFalse(_gate.CurrentCount == 0);
// We need the data from the accumulator across the lambda callbacks to SetCurrentSolutionAsync, so declare
// it here. It will be assigned in `transformation:` below (which may happen multiple times if the
// transformation needs to rerun). Once the transformation succeeds and is applied, the
// 'onBeforeUpdate/onAfterUpdate' callbacks will be called, and can use the last assigned value in
// `transformation`.
SolutionChangeAccumulator solutionChanges = null!;
ProjectUpdateState projectUpdateState = null!;
var (didUpdate, newSolution) = await Workspace.SetCurrentSolutionAsync(
useAsync,
transformation: oldSolution =>
{
solutionChanges = new SolutionChangeAccumulator(oldSolution);
// Use the _projectUpdateState here to ensure retries run with the original state.
projectUpdateState = mutation(solutionChanges, _projectUpdateState);
// Note: If the accumulator showed no changes it will return oldSolution. This ensures that
// SetCurrentSolutionAsync bails out immediately and no further work is done.
return solutionChanges.Solution;
},
changeKind: (_, _) => (solutionChanges.WorkspaceChangeKind, solutionChanges.WorkspaceChangeProjectId, solutionChanges.WorkspaceChangeDocumentId),
onBeforeUpdate: (_, _) =>
{
// Clear out mutable state not associated with the solution snapshot (for example, which documents are
// currently open).
foreach (var documentId in solutionChanges.DocumentIdsRemoved)
Workspace.ClearDocumentData(documentId);
},
onAfterUpdate: null,
CancellationToken.None).ConfigureAwait(false);
// Now that the project update has actually applied, we can apply the results of it.
// For example saving the state and updating file watchers for added/removed references.
//
// Importantly this is not done inside the SetCurrentSolution onAfterUpdate as that
// will only run *if* the transformation resulted in a changed solution, but this
// must run regardless (it is possible we update maps, but did not end up actually changing the sln object) in the transformation.
ApplyProjectUpdateState(projectUpdateState);
onAfterUpdateAlways?.Invoke(projectUpdateState);
}
private void ApplyBatchChangeToWorkspace_NoLock(
Func<SolutionChangeAccumulator, ProjectUpdateState, ProjectUpdateState> mutation, Action<ProjectUpdateState>? onAfterUpdateAlways)
{
Contract.ThrowIfFalse(_gate.CurrentCount == 0);
ApplyBatchChangeToWorkspaceMaybe_NoLockAsync(useAsync: false, mutation, onAfterUpdateAlways).VerifyCompleted();
}
private static ProjectUpdateState GetReferenceInformation(ProjectId projectId, ProjectUpdateState projectUpdateState, out ProjectReferenceInformation projectReference)
{
if (projectUpdateState.ProjectReferenceInfos.TryGetValue(projectId, out var referenceInfo))
{
projectReference = referenceInfo;
return projectUpdateState;
}
else
{
projectReference = new ProjectReferenceInformation([], []);
return projectUpdateState with
{
ProjectReferenceInfos = projectUpdateState.ProjectReferenceInfos.Add(projectId, projectReference)
};
}
}
/// <summary>
/// Removes the project from the various maps this type maintains; it's still up to the caller to actually remove
/// the project in one way or another.
/// </summary>
internal void RemoveProjectFromTrackingMaps_NoLock(ProjectId projectId)
{
Contract.ThrowIfFalse(_gate.CurrentCount == 0);
// This is set in the transformation function, but needs to be used by the onAfterUpdateAlways callback
// so we define it here outside of the lambda.
Project project = null!;
ApplyBatchChangeToWorkspace_NoLock((solutionChanges, projectUpdateState) =>
{
project = Workspace.CurrentSolution.GetRequiredProject(projectId);
if (projectUpdateState.ProjectReferenceInfos.TryGetValue(projectId, out var projectReferenceInfo))
{
// If we still had any output paths, we'll want to remove them to cause conversion back to metadata references.
// The call below implicitly is modifying the collection we've fetched, so we'll make a copy.
foreach (var outputPath in projectReferenceInfo.OutputPaths.ToList())
{
projectUpdateState = RemoveProjectOutputPath_NoLock(solutionChanges, projectId, outputPath, projectUpdateState, SolutionClosing, SolutionServices);
}
projectUpdateState = projectUpdateState with
{
ProjectReferenceInfos = projectUpdateState.ProjectReferenceInfos.Remove(projectId)
};
}
return projectUpdateState;
}, onAfterUpdateAlways: (projectUpdateState) =>
{
// This is called once after the above transformation is successfully applied.
ImmutableInterlocked.TryRemove<ProjectId, string?>(ref _projectToMaxSupportedLangVersionMap, projectId, out _);
ImmutableInterlocked.TryRemove(ref _projectToDependencyNodeTargetIdentifier, projectId, out _);
_onProjectRemoved?.Invoke(project);
});
}
internal void ApplyProjectUpdateState(ProjectUpdateState projectUpdateState)
{
Contract.ThrowIfFalse(_gate.CurrentCount == 0);
// Remove file watchers for any references we're no longer watching.
foreach (var reference in projectUpdateState.RemovedMetadataReferences)
FileWatchedPortableExecutableReferenceFactory.StopWatchingReference(reference.FilePath!, referenceToTrack: reference);
// Add file watchers for any references we are now watching.
foreach (var reference in projectUpdateState.AddedMetadataReferences)
FileWatchedPortableExecutableReferenceFactory.StartWatchingReference(reference.FilePath!);
// Remove file watchers for any references we're no longer watching.
foreach (var referenceFullPath in projectUpdateState.RemovedAnalyzerReferences)
FileWatchedAnalyzerReferenceFactory.StopWatchingReference(referenceFullPath, referenceToTrack: null);
// Add file watchers for any references we are now watching.
foreach (var referenceFullPath in projectUpdateState.AddedAnalyzerReferences)
FileWatchedAnalyzerReferenceFactory.StartWatchingReference(referenceFullPath);
// Clear the state from the this update in preparation for the next.
projectUpdateState = projectUpdateState.ClearIncrementalState();
_projectUpdateState = projectUpdateState;
}
internal void RemoveSolution_NoLock()
{
Contract.ThrowIfFalse(_gate.CurrentCount == 0);
// At this point, we should have had RemoveProjectFromTrackingMaps_NoLock called for everything else, so it's just the solution itself
// to clean up
Contract.ThrowIfFalse(_projectUpdateState.ProjectReferenceInfos.Count == 0);
Contract.ThrowIfFalse(_projectToMaxSupportedLangVersionMap.Count == 0);
Contract.ThrowIfFalse(_projectToDependencyNodeTargetIdentifier.Count == 0);
// Create a new empty solution and set this; we will reuse the same SolutionId and path since components
// still may have persistence information they still need to look up by that location; we also keep the
// existing analyzer references around since those are host-level analyzers that were loaded asynchronously.
Workspace.SetCurrentSolution(
solution => Workspace.CreateSolution(
SolutionInfo.Create(
SolutionId.CreateNewId(),
VersionStamp.Create(),
analyzerReferences: solution.AnalyzerReferences)),
WorkspaceChangeKind.SolutionRemoved,
onBeforeUpdate: (_, _) =>
{
Workspace.ClearOpenDocuments();
});
}
[PerformanceSensitive("https://github.com/dotnet/roslyn/issues/54137", AllowLocks = false)]
internal void SetMaxLanguageVersion(ProjectId projectId, string? maxLanguageVersion)
{
ImmutableInterlocked.Update(
ref _projectToMaxSupportedLangVersionMap,
static (map, arg) => map.SetItem(arg.projectId, arg.maxLanguageVersion),
(projectId, maxLanguageVersion));
}
[PerformanceSensitive("https://github.com/dotnet/roslyn/issues/54135", AllowLocks = false)]
internal void SetDependencyNodeTargetIdentifier(ProjectId projectId, string targetIdentifier)
{
ImmutableInterlocked.Update(
ref _projectToDependencyNodeTargetIdentifier,
static (map, arg) => map.SetItem(arg.projectId, arg.targetIdentifier),
(projectId, targetIdentifier));
}
public static ProjectUpdateState AddProjectOutputPath_NoLock(
SolutionChangeAccumulator solutionChanges,
ProjectId projectId,
string outputPath,
ProjectUpdateState projectUpdateState,
SolutionServices solutionServices)
{
projectUpdateState = GetReferenceInformation(projectId, projectUpdateState, out var projectReferenceInformation);
projectUpdateState = projectUpdateState.WithProjectReferenceInfo(projectId, projectReferenceInformation with
{
OutputPaths = projectReferenceInformation.OutputPaths.Add(outputPath)
});
projectUpdateState = projectUpdateState.WithProjectOutputPath(outputPath, projectId);
var projectsForOutputPath = projectUpdateState.ProjectsByOutputPath[outputPath];
var distinctProjectsForOutputPath = projectsForOutputPath.Distinct().ToList();
// If we have exactly one, then we're definitely good to convert
if (projectsForOutputPath.Count() == 1)
{
projectUpdateState = ConvertMetadataReferencesToProjectReferences_NoLock(solutionChanges, projectId, outputPath, projectUpdateState);
}
else if (distinctProjectsForOutputPath.Count == 1)
{
// The same project has multiple output paths that are the same. Any project would have already been converted
// by the prior add, so nothing further to do
}
else
{
// We have more than one project outputting to the same path. This shouldn't happen but we'll convert back
// because now we don't know which project to reference.
foreach (var otherProjectId in projectsForOutputPath)
{
// We know that since we're adding a path to projectId and we're here that we couldn't have already
// had a converted reference to us, instead we need to convert things that are pointing to the project
// we're colliding with
if (otherProjectId != projectId)
{
projectUpdateState = ConvertProjectReferencesToMetadataReferences_NoLock(solutionChanges, otherProjectId, outputPath, projectUpdateState, solutionServices);
}
}
}
return projectUpdateState;
}
/// <summary>
/// Attempts to convert all metadata references to <paramref name="outputPath"/> to a project reference to <paramref
/// name="projectIdToReference"/>.
/// </summary>
/// <param name="projectIdToReference">The <see cref="ProjectId"/> of the project that could be referenced in place
/// of the output path.</param>
/// <param name="outputPath">The output path to replace.</param>
[PerformanceSensitive("https://github.com/dotnet/roslyn/issues/31306",
Constraint = "Avoid calling " + nameof(CodeAnalysis.Solution.GetProject) + " to avoid realizing all projects.")]
private static ProjectUpdateState ConvertMetadataReferencesToProjectReferences_NoLock(
SolutionChangeAccumulator solutionChanges,
ProjectId projectIdToReference,
string outputPath,
ProjectUpdateState projectUpdateState)
{
foreach (var projectIdToRetarget in solutionChanges.Solution.ProjectIds)
{
if (CanConvertMetadataReferenceToProjectReference(solutionChanges.Solution, projectIdToRetarget, referencedProjectId: projectIdToReference))
{
// PERF: call GetRequiredProjectState instead of GetRequiredProject, otherwise creating a new project
// might force all Project instances to get created.
var projectState = solutionChanges.Solution.GetRequiredProjectState(projectIdToRetarget);
foreach (var reference in projectState.MetadataReferences)
{
if (reference is PortableExecutableReference peReference
&& string.Equals(peReference.FilePath, outputPath, StringComparison.OrdinalIgnoreCase))
{
projectUpdateState = projectUpdateState.WithIncrementalMetadataReferenceRemoved(peReference);
var projectReference = new ProjectReference(projectIdToReference, peReference.Properties.Aliases, peReference.Properties.EmbedInteropTypes);
var newSolution = solutionChanges.Solution
.RemoveMetadataReference(projectIdToRetarget, peReference)
.AddProjectReference(projectIdToRetarget, projectReference);
solutionChanges.UpdateSolutionForProjectAction(projectIdToRetarget, newSolution);
projectUpdateState = GetReferenceInformation(projectIdToRetarget, projectUpdateState, out var projectInfo);
projectUpdateState = projectUpdateState.WithProjectReferenceInfo(projectIdToRetarget,
projectInfo.WithConvertedProjectReference(peReference.FilePath!, projectReference));
// We have converted one, but you could have more than one reference with different aliases that
// we need to convert, so we'll keep going
}
}
}
}
return projectUpdateState;
}
[PerformanceSensitive("https://github.com/dotnet/roslyn/issues/31306",
Constraint = "Avoid calling " + nameof(CodeAnalysis.Solution.GetProject) + " to avoid realizing all projects.")]
private static bool CanConvertMetadataReferenceToProjectReference(Solution solution, ProjectId projectIdWithMetadataReference, ProjectId referencedProjectId)
{
// We can never make a project reference ourselves. This isn't a meaningful scenario, but if somebody does this by accident
// we do want to throw exceptions.
if (projectIdWithMetadataReference == referencedProjectId)
{
return false;
}
// PERF: call GetProjectState instead of GetProject, otherwise creating a new project might force all
// Project instances to get created.
var projectWithMetadataReference = solution.GetProjectState(projectIdWithMetadataReference);
var referencedProject = solution.GetProjectState(referencedProjectId);
Contract.ThrowIfNull(projectWithMetadataReference);
Contract.ThrowIfNull(referencedProject);
// We don't want to convert a metadata reference to a project reference if the project being referenced isn't something
// we can create a Compilation for. For example, if we have a C# project, and it's referencing a F# project via a metadata reference
// everything would be fine if we left it a metadata reference. Converting it to a project reference means we couldn't create a Compilation
// anymore in the IDE, since the C# compilation would need to reference an F# compilation. F# projects referencing other F# projects though
// do expect this to work, and so we'll always allow references through of the same language.
if (projectWithMetadataReference.Language != referencedProject.Language)
{
if (projectWithMetadataReference.LanguageServices.GetService<ICompilationFactoryService>() != null &&
referencedProject.LanguageServices.GetService<ICompilationFactoryService>() == null)
{
// We're referencing something that we can't create a compilation from something that can, so keep the metadata reference
return false;
}
}
// If this is going to cause a circular reference, also disallow it
if (solution.GetProjectDependencyGraph().GetProjectsThatThisProjectTransitivelyDependsOn(referencedProjectId).Contains(projectIdWithMetadataReference))
{
return false;
}
return true;
}
/// <summary>
/// Finds all projects that had a project reference to <paramref name="projectId"/> and convert it back to a metadata reference.
/// </summary>
/// <param name="projectId">The <see cref="ProjectId"/> of the project being referenced.</param>
/// <param name="outputPath">The output path of the given project to remove the link to.</param>
[PerformanceSensitive(
"https://github.com/dotnet/roslyn/issues/37616",
Constraint = "Update ConvertedProjectReferences in place to avoid duplicate list allocations.")]
private static ProjectUpdateState ConvertProjectReferencesToMetadataReferences_NoLock(
SolutionChangeAccumulator solutionChanges,
ProjectId projectId,
string outputPath,
ProjectUpdateState projectUpdateState,
SolutionServices solutionServices)
{
foreach (var projectIdToRetarget in solutionChanges.Solution.ProjectIds)
{
projectUpdateState = GetReferenceInformation(projectIdToRetarget, projectUpdateState, out var referenceInfo);
// Update ConvertedProjectReferences in place to avoid duplicate list allocations
for (var i = 0; i < referenceInfo.ConvertedProjectReferences.Count(); i++)
{
var convertedReference = referenceInfo.ConvertedProjectReferences[i];
if (string.Equals(convertedReference.path, outputPath, StringComparison.OrdinalIgnoreCase) &&
convertedReference.ProjectReference.ProjectId == projectId)
{
var metadataReference = CreateMetadataReference_NoLock(
convertedReference.path,
new MetadataReferenceProperties(
aliases: convertedReference.ProjectReference.Aliases,
embedInteropTypes: convertedReference.ProjectReference.EmbedInteropTypes),
solutionServices);
projectUpdateState = projectUpdateState.WithIncrementalMetadataReferenceAdded(metadataReference);
var newSolution = solutionChanges.Solution.RemoveProjectReference(projectIdToRetarget, convertedReference.ProjectReference)
.AddMetadataReference(projectIdToRetarget, metadataReference);
solutionChanges.UpdateSolutionForProjectAction(projectIdToRetarget, newSolution);
referenceInfo = referenceInfo with
{
ConvertedProjectReferences = referenceInfo.ConvertedProjectReferences.RemoveAt(i)
};
projectUpdateState = projectUpdateState.WithProjectReferenceInfo(projectIdToRetarget, referenceInfo);
// We have converted one, but you could have more than one reference with different aliases
// that we need to convert, so we'll keep going. Make sure to decrement the index so we don't
// skip any items.
i--;
}
}
}
return projectUpdateState;
}
/// <summary>
/// Converts a metadata reference to a project reference if possible.
/// This must be safe to run multiple times for the same reference as it is called
/// during a workspace update (which will attempt to apply the update multiple times).
/// </summary>
public static ProjectUpdateState TryCreateConvertedProjectReference_NoLock(
ProjectId referencingProject,
string path,
MetadataReferenceProperties properties,
ProjectUpdateState projectUpdateState,
Solution currentSolution,
out ProjectReference? projectReference)
{
if (projectUpdateState.ProjectsByOutputPath.TryGetValue(path, out var ids) && ids.Distinct().Count() == 1)
{
var projectIdToReference = ids.First();
if (CanConvertMetadataReferenceToProjectReference(currentSolution, referencingProject, projectIdToReference))
{
projectReference = new ProjectReference(
projectIdToReference,
aliases: properties.Aliases,
embedInteropTypes: properties.EmbedInteropTypes);
projectUpdateState = GetReferenceInformation(referencingProject, projectUpdateState, out var projectReferenceInfo);
projectUpdateState = projectUpdateState.WithProjectReferenceInfo(referencingProject, projectReferenceInfo.WithConvertedProjectReference(path, projectReference));
return projectUpdateState;
}
else
{
projectReference = null;
return projectUpdateState;
}
}
else
{
projectReference = null;
return projectUpdateState;
}
}
/// <summary>
/// Tries to convert a metadata reference to remove to a project reference.
/// </summary>
public static ProjectUpdateState TryRemoveConvertedProjectReference_NoLock(
ProjectId referencingProject,
string path,
MetadataReferenceProperties properties,
ProjectUpdateState projectUpdateState,
out ProjectReference? projectReference)
{
projectUpdateState = GetReferenceInformation(referencingProject, projectUpdateState, out var projectReferenceInformation);
foreach (var convertedProject in projectReferenceInformation.ConvertedProjectReferences)
{
if (convertedProject.path == path &&
convertedProject.ProjectReference.EmbedInteropTypes == properties.EmbedInteropTypes &&
convertedProject.ProjectReference.Aliases.SequenceEqual(properties.Aliases))
{
projectUpdateState = projectUpdateState.WithProjectReferenceInfo(referencingProject, projectReferenceInformation with
{
ConvertedProjectReferences = projectReferenceInformation.ConvertedProjectReferences.Remove(convertedProject)
});
projectReference = convertedProject.ProjectReference;
return projectUpdateState;
}
}
projectReference = null;
return projectUpdateState;
}
public static ProjectUpdateState RemoveProjectOutputPath_NoLock(
SolutionChangeAccumulator solutionChanges,
ProjectId projectId,
string outputPath,
ProjectUpdateState projectUpdateState,
bool solutionClosing,
SolutionServices solutionServices)
{
projectUpdateState = GetReferenceInformation(projectId, projectUpdateState, out var projectReferenceInformation);
if (!projectReferenceInformation.OutputPaths.Contains(outputPath))
{
throw new ArgumentException($"Project does not contain output path '{outputPath}'", nameof(outputPath));
}
projectUpdateState = projectUpdateState.WithProjectReferenceInfo(projectId, projectReferenceInformation with
{
OutputPaths = projectReferenceInformation.OutputPaths.Remove(outputPath)
});
projectUpdateState = projectUpdateState.RemoveProjectOutputPath(outputPath, projectId);
// When a project is closed, we may need to convert project references to metadata references (or vice
// versa). Failure to convert the references could leave a project in the workspace with a project
// reference to a project which is not open.
//
// For the specific case where the entire solution is closing, we do not need to update the state for
// remaining projects as each project closes, because we know those projects will be closed without
// further use. Avoiding reference conversion when the solution is closing improves performance for both
// IDE close scenarios and solution reload scenarios that occur after complex branch switches.
if (!solutionClosing)
{
if (projectUpdateState.ProjectsByOutputPath.TryGetValue(outputPath, out var remainingProjectsForOutputPath))
{
var distinctRemainingProjects = remainingProjectsForOutputPath.Distinct();
if (distinctRemainingProjects.Length == 1)
{
// We had more than one project outputting to the same path. Now we're back down to one
// so we can reference that one again
projectUpdateState = ConvertMetadataReferencesToProjectReferences_NoLock(solutionChanges, distinctRemainingProjects.Single(), outputPath, projectUpdateState);
}
}
else
{
// No projects left, we need to convert back to metadata references
projectUpdateState = ConvertProjectReferencesToMetadataReferences_NoLock(solutionChanges, projectId, outputPath, projectUpdateState, solutionServices);
}
}
return projectUpdateState;
}
/// <summary>
/// Gets or creates a PortableExecutableReference instance for the given file path and properties.
/// Calls to this are expected to be serialized by the caller.
/// </summary>
public static PortableExecutableReference CreateMetadataReference_NoLock(
string fullFilePath, MetadataReferenceProperties properties, SolutionServices solutionServices)
{
return solutionServices.GetRequiredService<IMetadataService>().GetReference(fullFilePath, properties);
}
private Task StartRefreshingMetadataReferencesForFileAsync(string fullFilePath, CancellationToken cancellationToken)
=> StartRefreshingReferencesForFileAsync(
fullFilePath,
getReferences: static project => project.MetadataReferences.OfType<PortableExecutableReference>(),
getFilePath: static reference => reference.FilePath!,
createNewReference: static (solutionServices, reference) => CreateMetadataReference_NoLock(reference.FilePath!, reference.Properties, solutionServices),
update: static (solution, projectId, projectUpdateState, oldReference, newReference) =>
{
var newSolution = solution
.RemoveMetadataReference(projectId, oldReference)
.AddMetadataReference(projectId, newReference);
var newProjectUpdateState = projectUpdateState
.WithIncrementalMetadataReferenceRemoved(oldReference)
.WithIncrementalMetadataReferenceAdded(newReference);
return (newSolution, newProjectUpdateState);
},
cancellationToken);
private Task StartRefreshingAnalyzerReferenceForFileAsync(string fullFilePath, CancellationToken cancellationToken)
=> StartRefreshingReferencesForFileAsync(
fullFilePath,
getReferences: static project => project.AnalyzerReferences.Select(r => r.FullPath!),
getFilePath: static filePath => filePath,
createNewReference: static (_, filePath) => filePath,
update: static (solution, projectId, projectUpdateState, oldAnalyzerFilePath, newAnalyzerFilePath) =>
{
// Note: we're passing in the same path for the analyzers to remove/add. That's exactly the intent
// here. We're updating an existing analyzer in place. The call to UpdateProjectAnalyzerReferences will
// preserve all the other analyzers (with a different path), remove the one with this path, make a new
// analyzer for this path, and then created an isolated ALC to load them all in.
Contract.ThrowIfTrue(oldAnalyzerFilePath != newAnalyzerFilePath);
var (newSolution, newProjectUpdateState) = ProjectSystemProject.UpdateProjectAnalyzerReferences(
solution, projectId, projectUpdateState, [oldAnalyzerFilePath], [newAnalyzerFilePath]);
return (newSolution, newProjectUpdateState);
},
cancellationToken);
/// <summary>
/// Core helper that handles refreshing the references we have for a particular <see
/// cref="PortableExecutableReference"/> or <see cref="AnalyzerFileReference"/>.
/// </summary>
private async Task StartRefreshingReferencesForFileAsync<TReference>(
string fullFilePath,
Func<Project, IEnumerable<TReference>> getReferences,
Func<TReference, string> getFilePath,
Func<SolutionServices, TReference, TReference> createNewReference,
Func<Solution, ProjectId, ProjectUpdateState, TReference, TReference, (Solution newSolution, ProjectUpdateState newProjectUpdateState)> update,
CancellationToken cancellationToken)
where TReference : class
{
await ApplyBatchChangeToWorkspaceAsync((solutionChanges, projectUpdateState) =>
{
var initialSolution = solutionChanges.Solution;
var solutionServices = initialSolution.Services;
foreach (var project in initialSolution.Projects)
{
// Loop to find each reference with the given path. It's possible that there might be multiple
// references of the same path; the project system could conceivably add the same reference multiple
// times but with different aliases. It's also possible we might not find the path at all: when we
// receive the file changed event, we aren't checking if the file is still in the workspace at that
// time; it's possible it might have already been removed.
foreach (var oldReference in getReferences(project))
{
cancellationToken.ThrowIfCancellationRequested();
if (fullFilePath.Equals(getFilePath(oldReference), StringComparison.OrdinalIgnoreCase))
{
var newSolution = solutionChanges.Solution;
(newSolution, projectUpdateState) = update(
newSolution, project.Id, projectUpdateState, oldReference, createNewReference(solutionServices, oldReference));
solutionChanges.UpdateSolutionForProjectAction(project.Id, newSolution);
}
}
}
return projectUpdateState;
}, onAfterUpdateAlways: null).ConfigureAwait(false);
}
internal Task RaiseOnDocumentsAddedMaybeAsync(bool useAsync, ImmutableArray<string> filePaths)
{
return _onDocumentsAddedMaybeAsync(useAsync, filePaths);
}
}
|