// 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.
#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Logging;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Serialization;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis
{
///
/// Represents a set of projects and their source code documents.
///
/// this is a green node of Solution like ProjectState/DocumentState are for
/// Project and Document.
///
internal partial class SolutionState
{
// branch id for this solution
private readonly BranchId _branchId;
// the version of the workspace this solution is from
private readonly int _workspaceVersion;
private readonly SolutionInfo.SolutionAttributes _solutionAttributes;
private readonly SolutionServices _solutionServices;
private readonly IReadOnlyList _projectIds;
private readonly ImmutableDictionary _projectIdToProjectStateMap;
private readonly ImmutableDictionary> _filePathToDocumentIdsMap;
private readonly ProjectDependencyGraph _dependencyGraph;
// Values for all these are created on demand.
private ImmutableDictionary _projectIdToTrackerMap;
// Checksums for this solution state
private readonly ValueSource _lazyChecksums;
private SolutionState(
BranchId branchId,
int workspaceVersion,
SolutionServices solutionServices,
SolutionInfo.SolutionAttributes solutionAttributes,
IEnumerable projectIds,
SerializableOptionSet options,
ImmutableDictionary idToProjectStateMap,
ImmutableDictionary projectIdToTrackerMap,
ImmutableDictionary> filePathToDocumentIdsMap,
ProjectDependencyGraph dependencyGraph)
{
_branchId = branchId;
_workspaceVersion = workspaceVersion;
_solutionAttributes = solutionAttributes;
_solutionServices = solutionServices;
_projectIds = projectIds.ToImmutableReadOnlyListOrEmpty();
Options = options ?? throw new ArgumentNullException(nameof(options));
_projectIdToProjectStateMap = idToProjectStateMap;
_projectIdToTrackerMap = projectIdToTrackerMap;
_filePathToDocumentIdsMap = filePathToDocumentIdsMap;
_dependencyGraph = dependencyGraph;
// when solution state is changed, we re-calcuate its checksum
_lazyChecksums = new AsyncLazy(ComputeChecksumsAsync, cacheResult: true);
CheckInvariants();
}
public SolutionState(
BranchId primaryBranchId,
SolutionServices solutionServices,
SolutionInfo.SolutionAttributes solutionAttributes,
SerializableOptionSet options)
: this(
primaryBranchId,
workspaceVersion: 0,
solutionServices,
solutionAttributes,
projectIds: ImmutableArray.Empty,
options,
idToProjectStateMap: ImmutableDictionary.Empty,
projectIdToTrackerMap: ImmutableDictionary.Empty,
filePathToDocumentIdsMap: ImmutableDictionary.Create>(StringComparer.OrdinalIgnoreCase),
dependencyGraph: ProjectDependencyGraph.Empty)
{
}
public SolutionState WithNewWorkspace(Workspace workspace, int workspaceVersion)
{
var services = workspace != _solutionServices.Workspace
? new SolutionServices(workspace)
: _solutionServices;
// Note: this will potentially have problems if the workspace services are different, as some services
// get locked-in by document states and project states when first constructed.
return CreatePrimarySolution(branchId: workspace.PrimaryBranchId, workspaceVersion: workspaceVersion, services: services);
}
public SolutionInfo.SolutionAttributes SolutionAttributes => _solutionAttributes;
public ImmutableDictionary ProjectStates => _projectIdToProjectStateMap;
public int WorkspaceVersion => _workspaceVersion;
public SolutionServices Services => _solutionServices;
public SerializableOptionSet Options { get; }
///
/// branch id of this solution
///
/// currently, it only supports one level of branching. there is a primary branch of a workspace and all other
/// branches that are branched from the primary branch.
///
/// one still can create multiple forked solutions from an already branched solution, but versions among those
/// can't be reliably used and compared.
///
/// version only has a meaning between primary solution and branched one or between solutions from same branch.
///
public BranchId BranchId => _branchId;
///
/// The Workspace this solution is associated with.
///
public Workspace Workspace => _solutionServices.Workspace;
///
/// The Id of the solution. Multiple solution instances may share the same Id.
///
public SolutionId Id => _solutionAttributes.Id;
///
/// The path to the solution file or null if there is no solution file.
///
public string? FilePath => _solutionAttributes.FilePath;
///
/// The solution version. This equates to the solution file's version.
///
public VersionStamp Version => _solutionAttributes.Version;
///
/// A list of all the ids for all the projects contained by the solution.
///
public IReadOnlyList ProjectIds => _projectIds;
// [Conditional("DEBUG")]
private void CheckInvariants()
{
Contract.ThrowIfTrue(_projectIds.Count != _projectIdToProjectStateMap.Count);
// An id shouldn't point at a tracker for a different project.
Contract.ThrowIfTrue(_projectIdToTrackerMap.Any(kvp => kvp.Key != kvp.Value.ProjectState.Id));
}
private SolutionState Branch(
SolutionInfo.SolutionAttributes? solutionAttributes = null,
IEnumerable? projectIds = null,
SerializableOptionSet? options = null,
ImmutableDictionary? idToProjectStateMap = null,
ImmutableDictionary? projectIdToTrackerMap = null,
ImmutableDictionary>? filePathToDocumentIdsMap = null,
ProjectDependencyGraph? dependencyGraph = null)
{
var branchId = GetBranchId();
solutionAttributes ??= _solutionAttributes;
projectIds ??= _projectIds;
idToProjectStateMap ??= _projectIdToProjectStateMap;
options ??= Options.WithLanguages(GetProjectLanguages(idToProjectStateMap));
projectIdToTrackerMap ??= _projectIdToTrackerMap;
filePathToDocumentIdsMap ??= _filePathToDocumentIdsMap;
dependencyGraph ??= _dependencyGraph;
if (branchId == _branchId &&
solutionAttributes == _solutionAttributes &&
projectIds == _projectIds &&
options == Options &&
idToProjectStateMap == _projectIdToProjectStateMap &&
projectIdToTrackerMap == _projectIdToTrackerMap &&
filePathToDocumentIdsMap == _filePathToDocumentIdsMap &&
dependencyGraph == _dependencyGraph)
{
return this;
}
return new SolutionState(
branchId,
_workspaceVersion,
_solutionServices,
solutionAttributes,
projectIds,
options,
idToProjectStateMap,
projectIdToTrackerMap,
filePathToDocumentIdsMap,
dependencyGraph);
}
private SolutionState CreatePrimarySolution(
BranchId branchId,
int workspaceVersion,
SolutionServices services)
{
if (branchId == _branchId &&
workspaceVersion == _workspaceVersion &&
services == _solutionServices)
{
return this;
}
return new SolutionState(
branchId,
workspaceVersion,
services,
_solutionAttributes,
_projectIds,
Options,
_projectIdToProjectStateMap,
_projectIdToTrackerMap,
_filePathToDocumentIdsMap,
_dependencyGraph);
}
private BranchId GetBranchId()
{
// currently we only support one level branching.
// my reasonings are
// 1. it seems there is no-one who needs sub branches.
// 2. this lets us to branch without explicit branch API
return _branchId == Workspace.PrimaryBranchId ? BranchId.GetNextId() : _branchId;
}
///
/// The version of the most recently modified project.
///
public VersionStamp GetLatestProjectVersion()
{
// this may produce a version that is out of sync with the actual Document versions.
var latestVersion = VersionStamp.Default;
foreach (var project in this.ProjectStates.Values)
{
latestVersion = project.Version.GetNewerVersion(latestVersion);
}
return latestVersion;
}
///
/// True if the solution contains a project with the specified project ID.
///
public bool ContainsProject([NotNullWhen(returnValue: true)] ProjectId? projectId)
{
return projectId != null && _projectIdToProjectStateMap.ContainsKey(projectId);
}
///
/// True if the solution contains the document in one of its projects
///
public bool ContainsDocument([NotNullWhen(returnValue: true)] DocumentId? documentId)
{
return
documentId != null &&
this.ContainsProject(documentId.ProjectId) &&
this.GetProjectState(documentId.ProjectId)!.ContainsDocument(documentId);
}
///
/// True if the solution contains the additional document in one of its projects
///
public bool ContainsAdditionalDocument([NotNullWhen(returnValue: true)] DocumentId? documentId)
{
return
documentId != null &&
this.ContainsProject(documentId.ProjectId) &&
this.GetProjectState(documentId.ProjectId)!.ContainsAdditionalDocument(documentId);
}
///
/// True if the solution contains the analyzer config document in one of its projects
///
public bool ContainsAnalyzerConfigDocument([NotNullWhen(returnValue: true)] DocumentId? documentId)
{
return
documentId != null &&
this.ContainsProject(documentId.ProjectId) &&
this.GetProjectState(documentId.ProjectId)!.ContainsAnalyzerConfigDocument(documentId);
}
private DocumentState? GetDocumentState(DocumentId? documentId)
{
if (documentId != null)
{
var projectState = this.GetProjectState(documentId.ProjectId);
if (projectState != null)
{
return projectState.GetDocumentState(documentId);
}
}
return null;
}
private DocumentState? GetDocumentState(SyntaxTree? syntaxTree, ProjectId? projectId)
{
if (syntaxTree != null)
{
// is this tree known to be associated with a document?
var docId = DocumentState.GetDocumentIdForTree(syntaxTree);
if (docId != null && (projectId == null || docId.ProjectId == projectId))
{
// does this solution even have the document?
var document = this.GetDocumentState(docId);
if (document != null)
{
// does this document really have the syntax tree?
if (document.TryGetSyntaxTree(out var documentTree) && documentTree == syntaxTree)
{
return document;
}
}
}
}
return null;
}
private TextDocumentState? GetAdditionalDocumentState(DocumentId? documentId)
{
if (documentId != null)
{
var projectState = this.GetProjectState(documentId.ProjectId);
if (projectState != null)
{
return projectState.GetAdditionalDocumentState(documentId);
}
}
return null;
}
private AnalyzerConfigDocumentState? GetAnalyzerConfigDocumentState(DocumentId? documentId)
{
if (documentId != null)
{
var projectState = this.GetProjectState(documentId.ProjectId);
if (projectState != null)
{
return projectState.GetAnalyzerConfigDocumentState(documentId);
}
}
return null;
}
public Task GetDependentVersionAsync(ProjectId projectId, CancellationToken cancellationToken)
{
return this.GetCompilationTracker(projectId).GetDependentVersionAsync(this, cancellationToken);
}
public Task GetDependentSemanticVersionAsync(ProjectId projectId, CancellationToken cancellationToken)
{
return this.GetCompilationTracker(projectId).GetDependentSemanticVersionAsync(this, cancellationToken);
}
public ProjectState? GetProjectState(ProjectId projectId)
{
_projectIdToProjectStateMap.TryGetValue(projectId, out var state);
return state;
}
///
/// Gets the associated with an assembly symbol.
///
public ProjectState? GetProjectState(IAssemblySymbol? assemblySymbol)
{
if (assemblySymbol == null)
{
return null;
}
// TODO: Remove this loop when we add source assembly symbols to s_assemblyOrModuleSymbolToProjectMap
foreach (var (_, state) in _projectIdToProjectStateMap)
{
if (this.TryGetCompilation(state.Id, out var compilation))
{
// if the symbol is the compilation's assembly symbol, we are done
if (Equals(compilation.Assembly, assemblySymbol))
{
return state;
}
}
}
s_assemblyOrModuleSymbolToProjectMap.TryGetValue(assemblySymbol, out var id);
return id == null ? null : this.GetProjectState(id);
}
private bool TryGetCompilationTracker(ProjectId projectId, [NotNullWhen(returnValue: true)] out CompilationTracker? tracker)
{
return _projectIdToTrackerMap.TryGetValue(projectId, out tracker);
}
private static readonly Func s_createCompilationTrackerFunction = CreateCompilationTracker;
private static CompilationTracker CreateCompilationTracker(ProjectId projectId, SolutionState solution)
{
var projectState = solution.GetProjectState(projectId);
Contract.ThrowIfNull(projectState);
return new CompilationTracker(projectState);
}
private CompilationTracker GetCompilationTracker(ProjectId projectId)
{
if (!_projectIdToTrackerMap.TryGetValue(projectId, out var tracker))
{
tracker = ImmutableInterlocked.GetOrAdd(ref _projectIdToTrackerMap, projectId, s_createCompilationTrackerFunction, this);
}
return tracker;
}
private SolutionState AddProject(ProjectId projectId, ProjectState projectState)
{
// changed project list so, increment version.
var newSolutionAttributes = _solutionAttributes.WithVersion(this.Version.GetNewerVersion());
var newProjectIds = _projectIds.ToImmutableArray().Add(projectId);
var newStateMap = _projectIdToProjectStateMap.Add(projectId, projectState);
var newDependencyGraph = _dependencyGraph
.WithAdditionalProjects(SpecializedCollections.SingletonEnumerable(projectId))
.WithAdditionalProjectReferences(projectId,
projectState.ProjectReferences.Where(r => _projectIdToProjectStateMap.ContainsKey(r.ProjectId)).Select(r => r.ProjectId).ToList());
// It's possible that another project already in newStateMap has a reference to this project that we're adding, since we allow
// dangling references like that. If so, we'll need to link those in too.
foreach (var newState in newStateMap)
{
foreach (var projectReference in newState.Value.ProjectReferences)
{
if (projectReference.ProjectId == projectId)
{
newDependencyGraph = newDependencyGraph.WithAdditionalProjectReferences(newState.Key, ImmutableArray.Create(projectId));
break;
}
}
}
var newTrackerMap = CreateCompilationTrackerMap(projectId, newDependencyGraph);
var newFilePathToDocumentIdsMap = CreateFilePathToDocumentIdsMapWithAddedProject(newStateMap[projectId]);
return this.Branch(
solutionAttributes: newSolutionAttributes,
projectIds: newProjectIds,
idToProjectStateMap: newStateMap,
projectIdToTrackerMap: newTrackerMap,
filePathToDocumentIdsMap: newFilePathToDocumentIdsMap,
dependencyGraph: newDependencyGraph);
}
///
/// Create a new solution instance that includes a project with the specified project information.
///
public SolutionState AddProject(ProjectInfo projectInfo)
{
if (projectInfo == null)
{
throw new ArgumentNullException(nameof(projectInfo));
}
var projectId = projectInfo.Id;
var language = projectInfo.Language;
if (language == null)
{
throw new ArgumentNullException(nameof(language));
}
var displayName = projectInfo.Name;
if (displayName == null)
{
throw new ArgumentNullException(nameof(displayName));
}
CheckNotContainsProject(projectId);
var languageServices = this.Workspace.Services.GetLanguageServices(language);
if (languageServices == null)
{
throw new ArgumentException(string.Format(WorkspacesResources.The_language_0_is_not_supported, language));
}
var newProject = new ProjectState(projectInfo, languageServices, _solutionServices);
return this.AddProject(newProject.Id, newProject);
}
private ImmutableDictionary> CreateFilePathToDocumentIdsMapWithAddedProject(ProjectState projectState)
{
return CreateFilePathToDocumentIdsMapWithAddedDocuments(GetDocumentStates(projectState));
}
private ImmutableDictionary> CreateFilePathToDocumentIdsMapWithAddedDocuments(IEnumerable documentStates)
{
var builder = _filePathToDocumentIdsMap.ToBuilder();
foreach (var documentState in documentStates)
{
if (string.IsNullOrEmpty(documentState.FilePath))
{
continue;
}
builder[documentState.FilePath!] = builder.TryGetValue(documentState.FilePath!, out var documentIdsWithPath)
? documentIdsWithPath.Add(documentState.Id)
: ImmutableArray.Create(documentState.Id);
}
return builder.ToImmutable();
}
private static IEnumerable GetDocumentStates(ProjectState projectState)
=> projectState.DocumentStates.Values
.Concat(projectState.AdditionalDocumentStates.Values)
.Concat(projectState.AnalyzerConfigDocumentStates.Values);
///
/// Create a new solution instance without the project specified.
///
public SolutionState RemoveProject(ProjectId projectId)
{
if (projectId == null)
{
throw new ArgumentNullException(nameof(projectId));
}
CheckContainsProject(projectId);
// changed project list so, increment version.
var newSolutionAttributes = _solutionAttributes.WithVersion(this.Version.GetNewerVersion());
var newProjectIds = _projectIds.ToImmutableArray().Remove(projectId);
var newStateMap = _projectIdToProjectStateMap.Remove(projectId);
var newDependencyGraph = _dependencyGraph.WithProjectRemoved(projectId);
var newTrackerMap = CreateCompilationTrackerMap(projectId, newDependencyGraph);
var newFilePathToDocumentIdsMap = CreateFilePathToDocumentIdsMapWithRemovedProject(_projectIdToProjectStateMap[projectId]);
return this.Branch(
solutionAttributes: newSolutionAttributes,
projectIds: newProjectIds,
idToProjectStateMap: newStateMap,
projectIdToTrackerMap: newTrackerMap.Remove(projectId),
filePathToDocumentIdsMap: newFilePathToDocumentIdsMap,
dependencyGraph: newDependencyGraph);
}
private ImmutableDictionary> CreateFilePathToDocumentIdsMapWithRemovedProject(ProjectState projectState)
{
return CreateFilePathToDocumentIdsMapWithRemovedDocuments(GetDocumentStates(projectState));
}
private ImmutableDictionary> CreateFilePathToDocumentIdsMapWithRemovedDocuments(IEnumerable documentStates)
{
var builder = _filePathToDocumentIdsMap.ToBuilder();
foreach (var documentState in documentStates)
{
if (string.IsNullOrEmpty(documentState.FilePath))
{
continue;
}
if (!builder.TryGetValue(documentState.FilePath!, out var documentIdsWithPath) || !documentIdsWithPath.Contains(documentState.Id))
{
throw new ArgumentException($"The given documentId was not found in '{nameof(_filePathToDocumentIdsMap)}'.");
}
if (documentIdsWithPath.Length == 1)
{
builder.Remove(documentState.FilePath!);
}
else
{
builder[documentState.FilePath!] = documentIdsWithPath.Remove(documentState.Id);
}
}
return builder.ToImmutable();
}
///
/// Creates a new solution instance with the project specified updated to have the new
/// assembly name.
///
public SolutionState WithProjectAssemblyName(ProjectId projectId, string assemblyName)
{
if (projectId == null)
{
throw new ArgumentNullException(nameof(projectId));
}
if (assemblyName == null)
{
throw new ArgumentNullException(nameof(assemblyName));
}
CheckContainsProject(projectId);
var oldProject = this.GetProjectState(projectId)!;
var newProject = oldProject.UpdateAssemblyName(assemblyName);
if (oldProject == newProject)
{
return this;
}
return this.ForkProject(newProject, new CompilationTranslationAction.ProjectAssemblyNameAction(assemblyName));
}
///
/// Creates a new solution instance with the project specified updated to have the output file path.
///
public SolutionState WithProjectOutputFilePath(ProjectId projectId, string? outputFilePath)
{
if (projectId == null)
{
throw new ArgumentNullException(nameof(projectId));
}
CheckContainsProject(projectId);
var oldProjectState = this.GetProjectState(projectId)!;
var newProjectState = oldProjectState.UpdateOutputFilePath(outputFilePath);
if (oldProjectState == newProjectState)
{
return this;
}
return this.ForkProject(newProjectState);
}
///
/// Creates a new solution instance with the project specified updated to have the output file path.
///
public SolutionState WithProjectOutputRefFilePath(ProjectId projectId, string? outputRefFilePath)
{
if (projectId == null)
{
throw new ArgumentNullException(nameof(projectId));
}
CheckContainsProject(projectId);
var oldProjectState = this.GetProjectState(projectId)!;
var newProjectState = oldProjectState.UpdateOutputRefFilePath(outputRefFilePath);
if (oldProjectState == newProjectState)
{
return this;
}
return this.ForkProject(newProjectState);
}
///
/// Creates a new solution instance with the project specified updated to have the default namespace.
///
public SolutionState WithProjectDefaultNamespace(ProjectId projectId, string? defaultNamespace)
{
if (projectId == null)
{
throw new ArgumentNullException(nameof(projectId));
}
CheckContainsProject(projectId);
var oldProjectState = this.GetProjectState(projectId)!;
var newProjectState = oldProjectState.UpdateDefaultNamespace(defaultNamespace);
if (oldProjectState == newProjectState)
{
return this;
}
return this.ForkProject(newProjectState);
}
///
/// Creates a new solution instance with the project specified updated to have the name.
///
public SolutionState WithProjectName(ProjectId projectId, string name)
{
if (projectId == null)
{
throw new ArgumentNullException(nameof(projectId));
}
CheckContainsProject(projectId);
var oldProjectState = this.GetProjectState(projectId)!;
var newProjectState = oldProjectState.UpdateName(name);
if (oldProjectState == newProjectState)
{
return this;
}
return this.ForkProject(newProjectState);
}
///
/// Creates a new solution instance with the project specified updated to have the project file path.
///
public SolutionState WithProjectFilePath(ProjectId projectId, string? filePath)
{
if (projectId == null)
{
throw new ArgumentNullException(nameof(projectId));
}
CheckContainsProject(projectId);
var oldProjectState = this.GetProjectState(projectId)!;
var newProjectState = oldProjectState.UpdateFilePath(filePath);
if (oldProjectState == newProjectState)
{
return this;
}
return this.ForkProject(newProjectState);
}
///
/// Create a new solution instance with the project specified updated to have
/// the specified compilation options.
///
public SolutionState WithProjectCompilationOptions(ProjectId projectId, CompilationOptions options)
{
if (projectId == null)
{
throw new ArgumentNullException(nameof(projectId));
}
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
CheckContainsProject(projectId);
var oldProject = this.GetProjectState(projectId)!;
var newProject = oldProject.UpdateCompilationOptions(options);
if (oldProject == newProject)
{
return this;
}
return this.ForkProject(newProject, new CompilationTranslationAction.ProjectCompilationOptionsAction(options));
}
///
/// Create a new solution instance with the project specified updated to have
/// the specified parse options.
///
public SolutionState WithProjectParseOptions(ProjectId projectId, ParseOptions options)
{
if (projectId == null)
{
throw new ArgumentNullException(nameof(projectId));
}
Debug.Assert(this.ContainsProject(projectId));
var oldProject = this.GetProjectState(projectId)!;
var newProject = oldProject.UpdateParseOptions(options);
if (oldProject == newProject)
{
return this;
}
if (this.Workspace.PartialSemanticsEnabled)
{
// don't fork tracker with queued action since access via partial semantics can become inconsistent (throw).
// Since changing options is rare event, it is okay to start compilation building from scratch.
return this.ForkProject(newProject, forkTracker: false);
}
else
{
return this.ForkProject(newProject, new CompilationTranslationAction.ReplaceAllSyntaxTreesAction(newProject));
}
}
///
/// Update a new solution instance with a fork of the specified project.
///
/// this is a temporary workaround until editorconfig becomes real part of roslyn solution snapshot.
/// until then, this will explicitly fork current solution snapshot
///
internal SolutionState WithProjectOptionsChanged(ProjectId projectId)
{
if (projectId == null)
{
throw new ArgumentNullException(nameof(projectId));
}
Debug.Assert(this.ContainsProject(projectId));
return ForkProject(GetProjectState(projectId)!);
}
///
/// Create a new solution instance with the project specified updated to have
/// the specified hasAllInformation.
///
public SolutionState WithHasAllInformation(ProjectId projectId, bool hasAllInformation)
{
if (projectId == null)
{
throw new ArgumentNullException(nameof(projectId));
}
Debug.Assert(this.ContainsProject(projectId));
var oldProject = this.GetProjectState(projectId)!;
var newProject = oldProject.UpdateHasAllInformation(hasAllInformation);
if (oldProject == newProject)
{
return this;
}
// fork without any change on compilation.
return this.ForkProject(newProject);
}
///
/// Create a new solution instance with the project specified updated to have
/// the specified runAnalyzers.
///
public SolutionState WithRunAnalyzers(ProjectId projectId, bool runAnalyzers)
{
if (projectId == null)
{
throw new ArgumentNullException(nameof(projectId));
}
Debug.Assert(this.ContainsProject(projectId));
var oldProject = this.GetProjectState(projectId)!;
var newProject = oldProject.UpdateRunAnalyzers(runAnalyzers);
if (oldProject == newProject)
{
return this;
}
// fork without any change on compilation.
return this.ForkProject(newProject);
}
///
/// Create a new solution instance with the project specified updated to include
/// the specified project references.
///
public SolutionState AddProjectReferences(ProjectId projectId, IEnumerable projectReferences)
{
if (projectId == null)
{
throw new ArgumentNullException(nameof(projectId));
}
if (projectReferences == null)
{
throw new ArgumentNullException(nameof(projectReferences));
}
if (!projectReferences.Any())
{
return this;
}
CheckContainsProject(projectId);
foreach (var referencedProject in projectReferences)
{
CheckContainsProject(referencedProject.ProjectId);
CheckNotContainsProjectReference(projectId, referencedProject);
CheckNotContainsTransitiveReference(referencedProject.ProjectId, projectId);
CheckNotSecondSubmissionReference(projectId, referencedProject.ProjectId);
}
var oldProject = this.GetProjectState(projectId)!;
var newProject = oldProject.AddProjectReferences(projectReferences);
var newDependencyGraph = _dependencyGraph.WithAdditionalProjectReferences(projectId, projectReferences.Select(r => r.ProjectId).ToList());
return this.ForkProject(newProject, newDependencyGraph: newDependencyGraph);
}
///
/// Create a new solution instance with the project specified updated to no longer
/// include the specified project reference.
///
public SolutionState RemoveProjectReference(ProjectId projectId, ProjectReference projectReference)
{
if (projectId == null)
{
throw new ArgumentNullException(nameof(projectId));
}
if (projectReference == null)
{
throw new ArgumentNullException(nameof(projectReference));
}
CheckContainsProject(projectId);
CheckContainsProject(projectReference.ProjectId);
var oldProject = this.GetProjectState(projectId)!;
var newProject = oldProject.RemoveProjectReference(projectReference);
ProjectDependencyGraph newDependencyGraph;
if (newProject.ContainsReferenceToProject(projectReference.ProjectId))
{
// The project contained multiple references to the project, and not all of them were removed. The
// dependency graph doesn't change.
newDependencyGraph = _dependencyGraph;
}
else
{
newDependencyGraph = _dependencyGraph.WithProjectReferenceRemoved(projectId, projectReference.ProjectId);
}
return this.ForkProject(newProject, newDependencyGraph: newDependencyGraph);
}
///
/// Create a new solution instance with the project specified updated to contain
/// the specified list of project references.
///
public SolutionState WithProjectReferences(ProjectId projectId, IEnumerable projectReferences)
{
if (projectId == null)
{
throw new ArgumentNullException(nameof(projectId));
}
if (projectReferences == null)
{
throw new ArgumentNullException(nameof(projectReferences));
}
CheckContainsProject(projectId);
var oldProject = this.GetProjectState(projectId)!;
var newProject = oldProject.WithProjectReferences(projectReferences);
return this.ForkProject(newProject, newDependencyGraph: _dependencyGraph.WithProjectReferences(projectId, projectReferences.Select(p => p.ProjectId)));
}
///
/// Creates a new solution instance with the project documents in the order by the specified document ids.
/// The specified document ids must be the same as what is already in the project; no adding or removing is allowed.
///
public SolutionState WithProjectDocumentsOrder(ProjectId projectId, ImmutableList documentIds)
{
if (projectId == null)
{
throw new ArgumentNullException(nameof(projectId));
}
if (documentIds == null)
{
throw new ArgumentNullException(nameof(documentIds));
}
CheckContainsProject(projectId);
var oldProject = this.GetProjectState(projectId)!;
var newProject = oldProject.UpdateDocumentsOrder(documentIds);
if (oldProject == newProject)
{
return this;
}
return this.ForkProject(newProject, new CompilationTranslationAction.ReplaceAllSyntaxTreesAction(newProject));
}
///
/// Create a new solution instance with the project specified updated to include the
/// specified metadata reference.
///
public SolutionState AddMetadataReference(ProjectId projectId, MetadataReference metadataReference)
{
if (projectId == null)
{
throw new ArgumentNullException(nameof(projectId));
}
if (metadataReference == null)
{
throw new ArgumentNullException(nameof(metadataReference));
}
CheckContainsProject(projectId);
return this.ForkProject(
this.GetProjectState(projectId)!.AddMetadataReference(metadataReference));
}
///
/// Create a new solution instance with the project specified updated to include the
/// specified metadata references.
///
public SolutionState AddMetadataReferences(ProjectId projectId, IEnumerable metadataReferences)
{
if (projectId == null)
{
throw new ArgumentNullException(nameof(projectId));
}
if (metadataReferences == null)
{
throw new ArgumentNullException(nameof(metadataReferences));
}
CheckContainsProject(projectId);
return this.ForkProject(this.GetProjectState(projectId)!.AddMetadataReferences(metadataReferences));
}
///
/// Create a new solution instance with the project specified updated to no longer include
/// the specified metadata reference.
///
public SolutionState RemoveMetadataReference(ProjectId projectId, MetadataReference metadataReference)
{
if (projectId == null)
{
throw new ArgumentNullException(nameof(projectId));
}
if (metadataReference == null)
{
throw new ArgumentNullException(nameof(metadataReference));
}
CheckContainsProject(projectId);
return this.ForkProject(
this.GetProjectState(projectId)!.RemoveMetadataReference(metadataReference));
}
///
/// Create a new solution instance with the project specified updated to include only the
/// specified metadata references.
///
public SolutionState WithProjectMetadataReferences(ProjectId projectId, IEnumerable metadataReferences)
{
if (projectId == null)
{
throw new ArgumentNullException(nameof(projectId));
}
if (metadataReferences == null)
{
throw new ArgumentNullException(nameof(metadataReferences));
}
CheckContainsProject(projectId);
return this.ForkProject(this.GetProjectState(projectId)!.WithMetadataReferences(metadataReferences));
}
///
/// Create a new solution instance with the project specified updated to include the
/// specified analyzer reference.
///
public SolutionState AddAnalyzerReference(ProjectId projectId, AnalyzerReference analyzerReference)
{
if (projectId == null)
{
throw new ArgumentNullException(nameof(projectId));
}
if (analyzerReference == null)
{
throw new ArgumentNullException(nameof(analyzerReference));
}
CheckContainsProject(projectId);
return this.ForkProject(this.GetProjectState(projectId)!.AddAnalyzerReference(analyzerReference));
}
///
/// Create a new solution instance with the project specified updated to include the
/// specified analyzer references.
///
public SolutionState AddAnalyzerReferences(ProjectId projectId, IEnumerable analyzerReferences)
{
if (projectId == null)
{
throw new ArgumentNullException(nameof(projectId));
}
if (analyzerReferences == null)
{
throw new ArgumentNullException(nameof(analyzerReferences));
}
if (!analyzerReferences.Any())
{
return this;
}
CheckContainsProject(projectId);
return this.ForkProject(this.GetProjectState(projectId)!.AddAnalyzerReferences(analyzerReferences));
}
///
/// Create a new solution instance with the project specified updated to no longer include
/// the specified analyzer reference.
///
public SolutionState RemoveAnalyzerReference(ProjectId projectId, AnalyzerReference analyzerReference)
{
if (projectId == null)
{
throw new ArgumentNullException(nameof(projectId));
}
if (analyzerReference == null)
{
throw new ArgumentNullException(nameof(analyzerReference));
}
CheckContainsProject(projectId);
return this.ForkProject(this.GetProjectState(projectId)!.RemoveAnalyzerReference(analyzerReference));
}
///
/// Create a new solution instance with the project specified updated to include only the
/// specified analyzer references.
///
public SolutionState WithProjectAnalyzerReferences(ProjectId projectId, IEnumerable analyzerReferences)
{
if (projectId == null)
{
throw new ArgumentNullException(nameof(projectId));
}
if (analyzerReferences == null)
{
throw new ArgumentNullException(nameof(analyzerReferences));
}
CheckContainsProject(projectId);
return this.ForkProject(this.GetProjectState(projectId)!.WithAnalyzerReferences(analyzerReferences));
}
///
/// Create a new solution instance with the corresponding projects updated to include new
/// documents defined by the document info.
///
public SolutionState AddDocuments(ImmutableArray documentInfos)
{
return AddDocumentsToMultipleProjects(documentInfos,
(documentInfo, project) => project.CreateDocument(documentInfo, project.ParseOptions),
(oldProject, documents) => (oldProject.AddDocuments(documents), new CompilationTranslationAction.AddDocumentsAction(documents)));
}
///
/// Core helper that takes a set of s and does the application of the appropriate documents to each project.
///
/// The set of documents to add.
/// Returns the new with the documents added, and the needed as well.
///
private SolutionState AddDocumentsToMultipleProjects(
ImmutableArray documentInfos,
Func createDocumentState,
Func, (ProjectState newState, CompilationTranslationAction? translationAction)> addDocumentsToProjectState)
where T : TextDocumentState
{
if (documentInfos.IsDefault)
{
throw new ArgumentNullException(nameof(documentInfos));
}
if (documentInfos.IsEmpty)
{
return this;
}
// The documents might be contributing to multiple different projects; split them by project and then we'll process
// project-at-a-time.
var documentInfosByProjectId = documentInfos.ToLookup(d => d.Id.ProjectId);
var newSolutionState = this;
foreach (var documentInfosInProject in documentInfosByProjectId)
{
CheckContainsProject(documentInfosInProject.Key);
var oldProject = this.GetProjectState(documentInfosInProject.Key)!;
var newDocumentStatesForProjectBuilder = ArrayBuilder.GetInstance();
foreach (var documentInfo in documentInfosInProject)
{
newDocumentStatesForProjectBuilder.Add(createDocumentState(documentInfo, oldProject));
}
var newDocumentStatesForProject = newDocumentStatesForProjectBuilder.ToImmutableAndFree();
var (newProjectState, compilationTranslationAction) = addDocumentsToProjectState(oldProject, newDocumentStatesForProject);
newSolutionState = newSolutionState.ForkProject(newProjectState,
compilationTranslationAction,
newFilePathToDocumentIdsMap: CreateFilePathToDocumentIdsMapWithAddedDocuments(newDocumentStatesForProject));
}
return newSolutionState;
}
public SolutionState AddAdditionalDocuments(ImmutableArray documentInfos)
{
return AddDocumentsToMultipleProjects(documentInfos,
(documentInfo, project) => new TextDocumentState(documentInfo, _solutionServices),
(projectState, documents) => (projectState.AddAdditionalDocuments(documents), translationAction: null));
}
public SolutionState AddAnalyzerConfigDocuments(ImmutableArray documentInfos)
{
// Adding a new analyzer config potentially impacts all syntax trees and the diagnostic reporting information
// attached to them, so we'll just replace all syntax trees in that case.
return AddDocumentsToMultipleProjects(documentInfos,
(documentInfo, project) => new AnalyzerConfigDocumentState(documentInfo, _solutionServices),
(oldProject, documents) =>
{
var newProject = oldProject.AddAnalyzerConfigDocuments(documents);
return (newProject, new CompilationTranslationAction.ReplaceAllSyntaxTreesAction(newProject));
});
}
public SolutionState RemoveAnalyzerConfigDocument(DocumentId documentId)
{
CheckContainsAnalyzerConfigDocument(documentId);
var oldProject = this.GetProjectState(documentId.ProjectId)!;
var newProject = oldProject.RemoveAnalyzerConfigDocument(documentId);
var removedDocumentStates = SpecializedCollections.SingletonEnumerable(oldProject.GetAnalyzerConfigDocumentState(documentId)!);
return this.ForkProject(
newProject,
new CompilationTranslationAction.ReplaceAllSyntaxTreesAction(newProject),
newFilePathToDocumentIdsMap: CreateFilePathToDocumentIdsMapWithRemovedDocuments(removedDocumentStates));
}
///
/// Creates a new solution instance that no longer includes the specified document.
///
public SolutionState RemoveDocuments(ImmutableArray documentIds)
{
return RemoveDocumentsFromMultipleProjects(documentIds,
(projectState, documentId) => { CheckContainsDocument(documentId); return projectState.GetDocumentState(documentId)!; },
(projectState, documentIds, documentStates) => (projectState.RemoveDocuments(documentIds), new CompilationTranslationAction.RemoveDocumentsAction(documentStates)));
}
private SolutionState RemoveDocumentsFromMultipleProjects(
ImmutableArray documentIds,
Func getExistingTextDocumentState,
Func, ImmutableArray, (ProjectState newState, CompilationTranslationAction? translationAction)> removeDocumentsFromProjectState)
where T : TextDocumentState
{
if (documentIds.IsDefault)
{
throw new ArgumentNullException(nameof(documentIds));
}
if (documentIds.IsEmpty)
{
return this;
}
// The documents might be contributing to multiple different projects; split them by project and then we'll process
// project-at-a-time.
var documentIdsByProjectId = documentIds.ToLookup(id => id.ProjectId);
var newSolutionState = this;
foreach (var documentIdsInProject in documentIdsByProjectId)
{
var oldProject = this.GetProjectState(documentIdsInProject.Key);
if (oldProject == null)
{
throw new InvalidOperationException(string.Format(WorkspacesResources._0_is_not_part_of_the_workspace, documentIdsInProject.Key));
}
var removedDocumentStatesBuilder = ArrayBuilder.GetInstance();
foreach (var documentId in documentIdsInProject)
{
removedDocumentStatesBuilder.Add(getExistingTextDocumentState(oldProject, documentId));
}
var removedDocumentStatesForProject = removedDocumentStatesBuilder.ToImmutableAndFree();
var (newProjectState, compilationTranslationAction) = removeDocumentsFromProjectState(oldProject, documentIdsInProject.ToImmutableArray(), removedDocumentStatesForProject);
newSolutionState = newSolutionState.ForkProject(newProjectState,
compilationTranslationAction,
newFilePathToDocumentIdsMap: CreateFilePathToDocumentIdsMapWithRemovedDocuments(removedDocumentStatesForProject));
}
return newSolutionState;
}
///
/// Creates a new solution instance that no longer includes the specified additional document.
///
public SolutionState RemoveAdditionalDocument(DocumentId documentId)
{
CheckContainsAdditionalDocument(documentId);
var oldProject = this.GetProjectState(documentId.ProjectId)!;
var newProject = oldProject.RemoveAdditionalDocument(documentId);
var documentStates = SpecializedCollections.SingletonEnumerable(GetAdditionalDocumentState(documentId)!);
return this.ForkProject(newProject,
newFilePathToDocumentIdsMap: CreateFilePathToDocumentIdsMapWithRemovedDocuments(documentStates));
}
///
/// Creates a new solution instance with the document specified updated to have the specified name.
///
public SolutionState WithDocumentName(DocumentId documentId, string name)
{
if (documentId == null)
{
throw new ArgumentNullException(nameof(documentId));
}
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
var oldDocument = this.GetDocumentState(documentId)!;
var newDocument = oldDocument.UpdateName(name);
return this.WithDocumentState(newDocument);
}
///
/// Creates a new solution instance with the document specified updated to be contained in
/// the sequence of logical folders.
///
public SolutionState WithDocumentFolders(DocumentId documentId, IEnumerable folders)
{
if (documentId == null)
{
throw new ArgumentNullException(nameof(documentId));
}
if (folders == null)
{
throw new ArgumentNullException(nameof(folders));
}
var folderCollection = folders.WhereNotNull().ToReadOnlyCollection();
var oldDocument = this.GetDocumentState(documentId)!;
var newDocument = oldDocument.UpdateFolders(folderCollection);
return this.WithDocumentState(newDocument);
}
///
/// Creates a new solution instance with the document specified updated to have the specified file path.
///
public SolutionState WithDocumentFilePath(DocumentId documentId, string filePath)
{
if (documentId == null)
{
throw new ArgumentNullException(nameof(documentId));
}
// TODO: why? we support nullable file paths
if (filePath == null)
{
throw new ArgumentNullException(nameof(filePath));
}
var oldDocument = this.GetDocumentState(documentId)!;
var newDocument = oldDocument.UpdateFilePath(filePath);
return this.WithDocumentState(newDocument);
}
///
/// Creates a new solution instance with the document specified updated to have the text
/// specified.
///
public SolutionState WithDocumentText(DocumentId documentId, SourceText text, PreservationMode mode = PreservationMode.PreserveValue)
{
if (documentId == null)
{
throw new ArgumentNullException(nameof(documentId));
}
if (text == null)
{
throw new ArgumentNullException(nameof(text));
}
CheckContainsDocument(documentId);
var oldDocument = this.GetDocumentState(documentId)!;
if (oldDocument.TryGetText(out var oldText) && text == oldText)
{
return this;
}
return this.WithDocumentState(oldDocument.UpdateText(text, mode), textChanged: true);
}
///
/// Creates a new solution instance with the additional document specified updated to have the text
/// specified.
///
public SolutionState WithAdditionalDocumentText(DocumentId documentId, SourceText text, PreservationMode mode = PreservationMode.PreserveValue)
{
if (documentId == null)
{
throw new ArgumentNullException(nameof(documentId));
}
if (text == null)
{
throw new ArgumentNullException(nameof(text));
}
CheckContainsAdditionalDocument(documentId);
var oldDocument = this.GetAdditionalDocumentState(documentId)!;
if (oldDocument.TryGetText(out var oldText) && text == oldText)
{
return this;
}
var newSolution = this.WithAdditionalDocumentState(oldDocument.UpdateText(text, mode), textChanged: true);
return newSolution;
}
///
/// Creates a new solution instance with the document specified updated to have the text
/// specified.
///
public SolutionState WithAnalyzerConfigDocumentText(DocumentId documentId, SourceText text, PreservationMode mode = PreservationMode.PreserveValue)
{
if (documentId == null)
{
throw new ArgumentNullException(nameof(documentId));
}
if (text == null)
{
throw new ArgumentNullException(nameof(text));
}
CheckContainsAnalyzerConfigDocument(documentId);
var oldDocument = this.GetAnalyzerConfigDocumentState(documentId)!;
if (oldDocument.TryGetText(out var oldText) && text == oldText)
{
return this;
}
return this.WithAnalyzerConfigDocumentState(oldDocument.UpdateText(text, mode), textChanged: true);
}
///
/// Creates a new solution instance with the document specified updated to have the text
/// and version specified.
///
public SolutionState WithDocumentText(DocumentId documentId, TextAndVersion textAndVersion, PreservationMode mode = PreservationMode.PreserveValue)
{
if (documentId == null)
{
throw new ArgumentNullException(nameof(documentId));
}
if (textAndVersion == null)
{
throw new ArgumentNullException(nameof(textAndVersion));
}
CheckContainsDocument(documentId);
var oldDocument = this.GetDocumentState(documentId)!;
return WithDocumentState(oldDocument.UpdateText(textAndVersion, mode), textChanged: true);
}
///
/// Creates a new solution instance with the additional document specified updated to have the text
/// and version specified.
///
public SolutionState WithAdditionalDocumentText(DocumentId documentId, TextAndVersion textAndVersion, PreservationMode mode = PreservationMode.PreserveValue)
{
if (documentId == null)
{
throw new ArgumentNullException(nameof(documentId));
}
if (textAndVersion == null)
{
throw new ArgumentNullException(nameof(textAndVersion));
}
CheckContainsAdditionalDocument(documentId);
var oldDocument = this.GetAdditionalDocumentState(documentId)!;
return WithAdditionalDocumentState(oldDocument.UpdateText(textAndVersion, mode), textChanged: true);
}
///
/// Creates a new solution instance with the analyzer config document specified updated to have the text
/// and version specified.
///
public SolutionState WithAnalyzerConfigDocumentText(DocumentId documentId, TextAndVersion textAndVersion, PreservationMode mode = PreservationMode.PreserveValue)
{
if (documentId == null)
{
throw new ArgumentNullException(nameof(documentId));
}
if (textAndVersion == null)
{
throw new ArgumentNullException(nameof(textAndVersion));
}
CheckContainsAnalyzerConfigDocument(documentId);
var oldDocument = this.GetAnalyzerConfigDocumentState(documentId)!;
return WithAnalyzerConfigDocumentState(oldDocument.UpdateText(textAndVersion, mode), textChanged: true);
}
///
/// Creates a new solution instance with the document specified updated to have a syntax tree
/// rooted by the specified syntax node.
///
public SolutionState WithDocumentSyntaxRoot(DocumentId documentId, SyntaxNode root, PreservationMode mode = PreservationMode.PreserveValue)
{
if (documentId == null)
{
throw new ArgumentNullException(nameof(documentId));
}
if (root == null)
{
throw new ArgumentNullException(nameof(root));
}
CheckContainsDocument(documentId);
var oldDocument = this.GetDocumentState(documentId)!;
if (oldDocument.TryGetSyntaxTree(out var oldTree) &&
oldTree.TryGetRoot(out var oldRoot) &&
oldRoot == root)
{
return this;
}
return WithDocumentState(oldDocument.UpdateTree(root, mode), textChanged: true);
}
private static async Task UpdateDocumentInCompilationAsync(
Compilation compilation,
DocumentState oldDocument,
DocumentState newDocument,
CancellationToken cancellationToken)
{
return compilation.ReplaceSyntaxTree(
await oldDocument.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false),
await newDocument.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false));
}
///
/// Creates a new solution instance with the document specified updated to have the source
/// code kind specified.
///
public SolutionState WithDocumentSourceCodeKind(DocumentId documentId, SourceCodeKind sourceCodeKind)
{
if (!Enum.IsDefined(typeof(SourceCodeKind), sourceCodeKind))
{
throw new ArgumentOutOfRangeException(nameof(sourceCodeKind));
}
CheckContainsDocument(documentId);
var oldDocument = this.GetDocumentState(documentId)!;
if (oldDocument.SourceCodeKind == sourceCodeKind)
{
return this;
}
return WithDocumentState(oldDocument.UpdateSourceCodeKind(sourceCodeKind), textChanged: true);
}
public SolutionState WithDocumentTextLoader(DocumentId documentId, TextLoader loader, SourceText? text, PreservationMode mode)
{
CheckContainsDocument(documentId);
var oldDocument = this.GetDocumentState(documentId)!;
// assumes that text has changed. user could have closed a doc without saving and we are loading text from closed file with
// old content. also this should make sure we don't re-use latest doc version with data associated with opened document.
return this.WithDocumentState(oldDocument.UpdateText(loader, text, mode), textChanged: true, recalculateDependentVersions: true);
}
///
/// Creates a new solution instance with the additional document specified updated to have the text
/// supplied by the text loader.
///
public SolutionState WithAdditionalDocumentTextLoader(DocumentId documentId, TextLoader loader, PreservationMode mode)
{
CheckContainsAdditionalDocument(documentId);
var oldDocument = this.GetAdditionalDocumentState(documentId)!;
// assumes that text has changed. user could have closed a doc without saving and we are loading text from closed file with
// old content. also this should make sure we don't re-use latest doc version with data associated with opened document.
return this.WithAdditionalDocumentState(oldDocument.UpdateText(loader, mode), textChanged: true, recalculateDependentVersions: true);
}
///
/// Creates a new solution instance with the analyzer config document specified updated to have the text
/// supplied by the text loader.
///
public SolutionState WithAnalyzerConfigDocumentTextLoader(DocumentId documentId, TextLoader loader, PreservationMode mode)
{
CheckContainsAnalyzerConfigDocument(documentId);
var oldDocument = this.GetAnalyzerConfigDocumentState(documentId)!;
// assumes that text has changed. user could have closed a doc without saving and we are loading text from closed file with
// old content. also this should make sure we don't re-use latest doc version with data associated with opened document.
return this.WithAnalyzerConfigDocumentState(oldDocument.UpdateText(loader, mode), textChanged: true, recalculateDependentVersions: true);
}
private SolutionState WithDocumentState(DocumentState newDocument, bool textChanged = false, bool recalculateDependentVersions = false)
{
if (newDocument == null)
{
throw new ArgumentNullException(nameof(newDocument));
}
CheckContainsDocument(newDocument.Id);
if (newDocument == this.GetDocumentState(newDocument.Id))
{
// old and new documents are the same instance
return this;
}
return this.TouchDocument(newDocument.Id, p => p.UpdateDocument(newDocument, textChanged, recalculateDependentVersions));
}
private SolutionState TouchDocument(DocumentId documentId, Func touchProject)
{
Debug.Assert(this.ContainsDocument(documentId));
var oldProject = this.GetProjectState(documentId.ProjectId)!;
var newProject = touchProject(oldProject);
if (oldProject == newProject)
{
// old and new projects are the same instance
return this;
}
var oldDocument = oldProject.GetDocumentState(documentId);
var newDocument = newProject.GetDocumentState(documentId);
return this.ForkProject(newProject, new CompilationTranslationAction.TouchDocumentAction(oldDocument, newDocument));
}
private SolutionState WithAdditionalDocumentState(TextDocumentState newDocument, bool textChanged = false, bool recalculateDependentVersions = false)
{
if (newDocument == null)
{
throw new ArgumentNullException(nameof(newDocument));
}
CheckContainsAdditionalDocument(newDocument.Id);
if (newDocument == this.GetAdditionalDocumentState(newDocument.Id))
{
// old and new documents are the same instance
return this;
}
var oldProject = this.GetProjectState(newDocument.Id.ProjectId)!;
var newProject = oldProject.UpdateAdditionalDocument(newDocument, textChanged, recalculateDependentVersions);
if (oldProject == newProject)
{
// old and new projects are the same instance
return this;
}
return this.ForkProject(newProject);
}
private SolutionState WithAnalyzerConfigDocumentState(AnalyzerConfigDocumentState newDocument, bool textChanged = false, bool recalculateDependentVersions = false)
{
if (newDocument == null)
{
throw new ArgumentNullException(nameof(newDocument));
}
CheckContainsAnalyzerConfigDocument(newDocument.Id);
if (newDocument == this.GetAnalyzerConfigDocumentState(newDocument.Id))
{
// old and new documents are the same instance
return this;
}
var oldProject = this.GetProjectState(newDocument.Id.ProjectId)!;
var newProject = oldProject.UpdateAnalyzerConfigDocument(newDocument, textChanged, recalculateDependentVersions);
if (oldProject == newProject)
{
// old and new projects are the same instance
return this;
}
return this.ForkProject(newProject, new CompilationTranslationAction.ReplaceAllSyntaxTreesAction(newProject));
}
///
/// Creates a new snapshot with an updated project and an action that will produce a new
/// compilation matching the new project out of an old compilation. All dependent projects
/// are fixed-up if the change to the new project affects its public metadata, and old
/// dependent compilations are forgotten.
///
private SolutionState ForkProject(
ProjectState newProjectState,
CompilationTranslationAction? translate = null,
ProjectDependencyGraph? newDependencyGraph = null,
ImmutableDictionary>? newFilePathToDocumentIdsMap = null,
bool forkTracker = true)
{
var projectId = newProjectState.Id;
var newStateMap = _projectIdToProjectStateMap.SetItem(projectId, newProjectState);
newDependencyGraph ??= _dependencyGraph;
var newTrackerMap = CreateCompilationTrackerMap(projectId, newDependencyGraph);
// If we have a tracker for this project, then fork it as well (along with the
// translation action and store it in the tracker map.
if (newTrackerMap.TryGetValue(projectId, out var tracker))
{
newTrackerMap = newTrackerMap.Remove(projectId);
if (forkTracker)
{
newTrackerMap = newTrackerMap.Add(projectId, tracker.Fork(newProjectState, translate));
}
}
return this.Branch(
idToProjectStateMap: newStateMap,
projectIdToTrackerMap: newTrackerMap,
dependencyGraph: newDependencyGraph,
filePathToDocumentIdsMap: newFilePathToDocumentIdsMap ?? _filePathToDocumentIdsMap);
}
///
/// Gets the set of s in this with a
/// that matches the given file path.
///
public ImmutableArray GetDocumentIdsWithFilePath(string? filePath)
{
if (string.IsNullOrEmpty(filePath))
{
return ImmutableArray.Create();
}
return _filePathToDocumentIdsMap.TryGetValue(filePath!, out var documentIds)
? documentIds
: ImmutableArray.Create();
}
private static ProjectDependencyGraph CreateDependencyGraph(
IReadOnlyList projectIds,
ImmutableDictionary projectStates)
{
var map = projectStates.Values.Select(state => new KeyValuePair>(
state.Id,
state.ProjectReferences.Where(pr => projectStates.ContainsKey(pr.ProjectId)).Select(pr => pr.ProjectId).ToImmutableHashSet()))
.Where(pair => !pair.Value.IsEmpty)
.ToImmutableDictionary();
return new ProjectDependencyGraph(projectIds.ToImmutableHashSet(), map);
}
private ImmutableDictionary CreateCompilationTrackerMap(ProjectId changedProjectId, ProjectDependencyGraph dependencyGraph)
{
var builder = ImmutableDictionary.CreateBuilder();
IEnumerable? dependencies = null;
foreach (var (id, tracker) in _projectIdToTrackerMap)
{
if (!tracker.HasCompilation)
{
continue;
}
builder.Add(id, CanReuse(id) ? tracker : tracker.Fork(tracker.ProjectState));
}
return builder.ToImmutable();
// Returns true if 'tracker' can be reused for project 'id'
bool CanReuse(ProjectId id)
{
if (id == changedProjectId)
return true;
// Check the dependency graph to see if project 'id' directly or transitively depends on 'projectId'.
// If the information is not available, do not compute it.
var forwardDependencies = dependencyGraph.TryGetProjectsThatThisProjectTransitivelyDependsOn(id);
if (forwardDependencies is object && !forwardDependencies.Contains(changedProjectId))
{
return true;
}
// Compute the set of all projects that depend on 'projectId'. This information answers the same
// question as the previous check, but involves at most one transitive computation within the
// dependency graph.
dependencies ??= dependencyGraph.GetProjectsThatTransitivelyDependOnThisProject(changedProjectId);
return !dependencies.Contains(id);
}
}
///
/// Gets a copy of the solution isolated from the original so that they do not share computed state.
///
/// Use isolated solutions when doing operations that are likely to access a lot of text,
/// syntax trees or compilations that are unlikely to be needed again after the operation is done.
/// When the isolated solution is reclaimed so will the computed state.
///
public SolutionState GetIsolatedSolution()
{
var forkedMap = ImmutableDictionary.CreateRange(
_projectIdToTrackerMap.Where(kvp => kvp.Value.HasCompilation)
.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Clone())));
return this.Branch(projectIdToTrackerMap: forkedMap);
}
public SolutionState WithOptions(SerializableOptionSet options) => this.Branch(options: options);
// this lock guards all the mutable fields (do not share lock with derived classes)
private NonReentrantLock? _stateLockBackingField;
private NonReentrantLock StateLock
{
get
{
// TODO: why did I need to do a nullable suppression here?
return LazyInitializer.EnsureInitialized(ref _stateLockBackingField, NonReentrantLock.Factory)!;
}
}
private WeakReference? _latestSolutionWithPartialCompilation;
private DateTime _timeOfLatestSolutionWithPartialCompilation;
private DocumentId? _documentIdOfLatestSolutionWithPartialCompilation;
///
/// Creates a branch of the solution that has its compilations frozen in whatever state they are in at the time, assuming a background compiler is
/// busy building this compilations.
///
/// A compilation for the project containing the specified document id will be guaranteed to exist with at least the syntax tree for the document.
///
/// This not intended to be the public API, use Document.WithFrozenPartialSemantics() instead.
///
public SolutionState WithFrozenPartialCompilationIncludingSpecificDocument(DocumentId documentId, CancellationToken cancellationToken)
{
try
{
var doc = this.GetDocumentState(documentId)!;
var tree = doc.GetSyntaxTree(cancellationToken);
using (this.StateLock.DisposableWait(cancellationToken))
{
// in progress solutions are disabled for some testing
if (this.Workspace is Workspace ws && ws.TestHookPartialSolutionsDisabled)
{
return this;
}
SolutionState? currentPartialSolution = null;
if (_latestSolutionWithPartialCompilation != null)
{
_latestSolutionWithPartialCompilation.TryGetTarget(out currentPartialSolution);
}
var reuseExistingPartialSolution =
currentPartialSolution != null &&
(DateTime.UtcNow - _timeOfLatestSolutionWithPartialCompilation).TotalSeconds < 0.1 &&
_documentIdOfLatestSolutionWithPartialCompilation == documentId;
if (reuseExistingPartialSolution)
{
SolutionLogger.UseExistingPartialSolution();
return currentPartialSolution!;
}
// if we don't have one or it is stale, create a new partial solution
var tracker = this.GetCompilationTracker(documentId.ProjectId);
var newTracker = tracker.FreezePartialStateWithTree(this, doc, tree, cancellationToken);
var newIdToProjectStateMap = _projectIdToProjectStateMap.SetItem(documentId.ProjectId, newTracker.ProjectState);
var newIdToTrackerMap = _projectIdToTrackerMap.SetItem(documentId.ProjectId, newTracker);
currentPartialSolution = this.Branch(
idToProjectStateMap: newIdToProjectStateMap,
projectIdToTrackerMap: newIdToTrackerMap,
dependencyGraph: CreateDependencyGraph(_projectIds, newIdToProjectStateMap));
_latestSolutionWithPartialCompilation = new WeakReference(currentPartialSolution);
_timeOfLatestSolutionWithPartialCompilation = DateTime.UtcNow;
_documentIdOfLatestSolutionWithPartialCompilation = documentId;
SolutionLogger.CreatePartialSolution();
return currentPartialSolution;
}
}
catch (Exception e) when (FatalError.ReportUnlessCanceled(e))
{
throw ExceptionUtilities.Unreachable;
}
}
///
/// Creates a new solution instance with all the documents specified updated to have the same specified text.
///
public SolutionState WithDocumentText(IEnumerable documentIds, SourceText text, PreservationMode mode = PreservationMode.PreserveValue)
{
if (documentIds == null)
{
throw new ArgumentNullException(nameof(documentIds));
}
if (text == null)
{
throw new ArgumentNullException(nameof(text));
}
var solution = this;
foreach (var documentId in documentIds)
{
var doc = solution.GetDocumentState(documentId);
if (doc != null)
{
if (!doc.TryGetText(out var existingText) || existingText != text)
{
solution = solution.WithDocumentText(documentId, text, mode);
}
}
}
return solution;
}
public bool TryGetCompilation(ProjectId projectId, [NotNullWhen(returnValue: true)] out Compilation? compilation)
{
CheckContainsProject(projectId);
compilation = null;
return this.TryGetCompilationTracker(projectId, out var tracker)
&& tracker.TryGetCompilation(out compilation);
}
///
/// Returns the compilation for the specified . Can return when the project
/// does not support compilations.
///
///
/// The compilation is guaranteed to have a syntax tree for each document of the project.
///
private Task GetCompilationAsync(ProjectId projectId, CancellationToken cancellationToken)
{
// TODO: figure out where this is called and why the nullable suppression is required
return GetCompilationAsync(GetProjectState(projectId)!, cancellationToken);
}
///
/// Returns the compilation for the specified . Can return when the project
/// does not support compilations.
///
///
/// The compilation is guaranteed to have a syntax tree for each document of the project.
///
public Task GetCompilationAsync(ProjectState project, CancellationToken cancellationToken)
{
return project.SupportsCompilation
? GetCompilationTracker(project.Id).GetCompilationAsync(this, cancellationToken).AsNullable()
: SpecializedTasks.Null();
}
///
/// Return reference completeness for the given project and all projects this references.
///
public Task HasSuccessfullyLoadedAsync(ProjectState project, CancellationToken cancellationToken)
{
// return HasAllInformation when compilation is not supported.
// regardless whether project support compilation or not, if projectInfo is not complete, we can't guarantee its reference completeness
return project.SupportsCompilation
? this.GetCompilationTracker(project.Id).HasSuccessfullyLoadedAsync(this, cancellationToken)
: project.HasAllInformation ? SpecializedTasks.True : SpecializedTasks.False;
}
///
/// Symbols need to be either or .
///
private static readonly ConditionalWeakTable s_assemblyOrModuleSymbolToProjectMap =
new ConditionalWeakTable();
private static void RecordSourceOfAssemblySymbol(ISymbol? assemblyOrModuleSymbol, ProjectId projectId)
{
// TODO: how would we ever get a null here?
if (assemblyOrModuleSymbol == null)
{
return;
}
Contract.ThrowIfNull(projectId);
// remember which project is associated with this assembly
if (!s_assemblyOrModuleSymbolToProjectMap.TryGetValue(assemblyOrModuleSymbol, out var tmp))
{
// use GetValue to avoid race condition exceptions from Add.
// the first one to set the value wins.
s_assemblyOrModuleSymbolToProjectMap.GetValue(assemblyOrModuleSymbol, _ => projectId);
}
else
{
// sanity check: this should always be true, no matter how many times
// we attempt to record the association.
Debug.Assert(tmp == projectId);
}
}
///
/// Get a metadata reference for the project's compilation
///
public Task GetMetadataReferenceAsync(ProjectReference projectReference, ProjectState fromProject, CancellationToken cancellationToken)
{
try
{
// Get the compilation state for this project. If it's not already created, then this
// will create it. Then force that state to completion and get a metadata reference to it.
var tracker = this.GetCompilationTracker(projectReference.ProjectId);
return tracker.GetMetadataReferenceAsync(this, fromProject, projectReference, cancellationToken);
}
catch (Exception e) when (FatalError.ReportUnlessCanceled(e))
{
throw ExceptionUtilities.Unreachable;
}
}
///
/// Attempt to get the best readily available compilation for the project. It may be a
/// partially built compilation.
///
private MetadataReference? GetPartialMetadataReference(
ProjectReference projectReference,
ProjectState fromProject)
{
// Try to get the compilation state for this project. If it doesn't exist, don't do any
// more work.
if (!_projectIdToTrackerMap.TryGetValue(projectReference.ProjectId, out var state))
{
return null;
}
return state.GetPartialMetadataReference(fromProject, projectReference);
}
public async Task ContainsSymbolsWithNameAsync(ProjectId id, string name, SymbolFilter filter, CancellationToken cancellationToken)
{
var result = GetCompilationTracker(id).ContainsSymbolsWithNameFromDeclarationOnlyCompilation(name, filter, cancellationToken);
if (result.HasValue)
{
return result.Value;
}
// it looks like declaration compilation doesn't exist yet. we have to build full compilation
var compilation = await GetCompilationAsync(id, cancellationToken).ConfigureAwait(false);
if (compilation == null)
{
// some projects don't support compilations (e.g., TypeScript) so there's nothing to check
return false;
}
return compilation.ContainsSymbolsWithName(name, filter, cancellationToken);
}
public async Task ContainsSymbolsWithNameAsync(ProjectId id, Func predicate, SymbolFilter filter, CancellationToken cancellationToken)
{
var result = GetCompilationTracker(id).ContainsSymbolsWithNameFromDeclarationOnlyCompilation(predicate, filter, cancellationToken);
if (result.HasValue)
{
return result.Value;
}
// it looks like declaration compilation doesn't exist yet. we have to build full compilation
var compilation = await GetCompilationAsync(id, cancellationToken).ConfigureAwait(false);
if (compilation == null)
{
// some projects don't support compilations (e.g., TypeScript) so there's nothing to check
return false;
}
return compilation.ContainsSymbolsWithName(predicate, filter, cancellationToken);
}
public async Task> GetDocumentsWithNameAsync(
ProjectId id, Func predicate, SymbolFilter filter, CancellationToken cancellationToken)
{
// this will be used to find documents that contain declaration information in IDE cache such as DeclarationSyntaxTreeInfo for "NavigateTo"
var trees = GetCompilationTracker(id).GetSyntaxTreesWithNameFromDeclarationOnlyCompilation(predicate, filter, cancellationToken);
if (trees != null)
{
return ConvertTreesToDocuments(id, trees);
}
// it looks like declaration compilation doesn't exist yet. we have to build full compilation
var compilation = await GetCompilationAsync(id, cancellationToken).ConfigureAwait(false);
if (compilation == null)
{
// some projects don't support compilations (e.g., TypeScript) so there's nothing to check
return ImmutableArray.Empty;
}
return ConvertTreesToDocuments(
id, compilation.GetSymbolsWithName(predicate, filter, cancellationToken).SelectMany(s => s.DeclaringSyntaxReferences.Select(r => r.SyntaxTree)));
}
private ImmutableArray ConvertTreesToDocuments(ProjectId id, IEnumerable trees)
{
var result = ArrayBuilder.GetInstance();
foreach (var tree in trees)
{
var document = GetDocumentState(tree, id);
if (document == null)
{
// ignore trees that are not known to solution such as VB synthesized trees made by compilation.
continue;
}
result.Add(document);
}
return result.ToImmutableAndFree();
}
///
/// Gets a that details the dependencies between projects for this solution.
///
public ProjectDependencyGraph GetProjectDependencyGraph()
{
return _dependencyGraph;
}
private void CheckNotContainsProject(ProjectId projectId)
{
if (this.ContainsProject(projectId))
{
throw new InvalidOperationException(WorkspacesResources.The_solution_already_contains_the_specified_project);
}
}
private void CheckContainsProject(ProjectId projectId)
{
if (!this.ContainsProject(projectId))
{
throw new InvalidOperationException(WorkspacesResources.The_solution_does_not_contain_the_specified_project);
}
}
private void CheckNotContainsProjectReference(ProjectId projectId, ProjectReference referencedProject)
{
if (this.GetProjectState(projectId)!.ProjectReferences.Contains(referencedProject))
{
throw new InvalidOperationException(WorkspacesResources.The_project_already_references_the_target_project);
}
}
private void CheckNotContainsTransitiveReference(ProjectId fromProjectId, ProjectId toProjectId)
{
var dependents = _dependencyGraph.GetProjectsThatThisProjectTransitivelyDependsOn(fromProjectId);
if (dependents.Contains(toProjectId))
{
throw new InvalidOperationException(WorkspacesResources.The_project_already_transitively_references_the_target_project);
}
}
private void CheckNotSecondSubmissionReference(ProjectId projectId, ProjectId toProjectId)
{
var projectState = GetProjectState(projectId);
if (projectState!.IsSubmission && GetProjectState(toProjectId)!.IsSubmission)
{
if (projectState.ProjectReferences.Any(p => GetProjectState(p.ProjectId)!.IsSubmission))
{
throw new InvalidOperationException(WorkspacesResources.This_submission_already_references_another_submission_project);
}
}
}
private void CheckNotContainsDocument(DocumentId documentId)
{
Debug.Assert(!this.ContainsDocument(documentId));
if (this.ContainsDocument(documentId))
{
throw new InvalidOperationException(WorkspacesResources.The_solution_already_contains_the_specified_document);
}
}
private void CheckNotContainsAdditionalDocument(DocumentId documentId)
{
Debug.Assert(!this.ContainsAdditionalDocument(documentId));
if (this.ContainsAdditionalDocument(documentId))
{
throw new InvalidOperationException(WorkspacesResources.The_solution_already_contains_the_specified_document);
}
}
private void CheckContainsDocument(DocumentId documentId)
{
Debug.Assert(this.ContainsDocument(documentId));
if (!this.ContainsDocument(documentId))
{
throw new InvalidOperationException(WorkspaceExtensionsResources.The_solution_does_not_contain_the_specified_document);
}
}
private void CheckContainsAdditionalDocument(DocumentId documentId)
{
Debug.Assert(this.ContainsAdditionalDocument(documentId));
if (!this.ContainsAdditionalDocument(documentId))
{
throw new InvalidOperationException(WorkspaceExtensionsResources.The_solution_does_not_contain_the_specified_document);
}
}
private void CheckContainsAnalyzerConfigDocument(DocumentId documentId)
{
Debug.Assert(this.ContainsAnalyzerConfigDocument(documentId));
if (!this.ContainsAnalyzerConfigDocument(documentId))
{
throw new InvalidOperationException(WorkspaceExtensionsResources.The_solution_does_not_contain_the_specified_document);
}
}
internal ImmutableHashSet GetProjectLanguages()
=> GetProjectLanguages(ProjectStates);
private static ImmutableHashSet GetProjectLanguages(ImmutableDictionary projectStates)
=> projectStates.Select(p => p.Value.Language).ToImmutableHashSet();
}
}