|
// 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.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.Collections;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
using ReferenceEqualityComparer = Roslyn.Utilities.ReferenceEqualityComparer;
namespace Microsoft.CodeAnalysis;
internal sealed partial class SolutionCompilationState
{
/// <summary>
/// Symbols need to be either <see cref="IAssemblySymbol"/> or <see cref="IModuleSymbol"/>.
/// </summary>
private static readonly ConditionalWeakTable<ISymbol, ProjectId> s_assemblyOrModuleSymbolToProjectMap = new();
/// <summary>
/// Green version of the information about this Solution instance. Responsible for non-semantic information
/// about the solution structure. Specifically, the set of green <see cref="ProjectState"/>s, with all their
/// green <see cref="DocumentState"/>s. Contains the attributes, options and relationships between projects.
/// Effectively, everything specified in a project file. Does not contain anything related to <see
/// cref="Compilation"/>s or semantics.
/// </summary>
public SolutionState SolutionState { get; }
public bool PartialSemanticsEnabled { get; }
public TextDocumentStates<SourceGeneratedDocumentState>? FrozenSourceGeneratedDocumentStates { get; }
// Values for all these are created on demand.
private ImmutableSegmentedDictionary<ProjectId, ICompilationTracker> _projectIdToTrackerMap;
/// <summary>
/// Cache we use to map between unrooted symbols (i.e. assembly, module and dynamic symbols) and the project
/// they came from. That way if we are asked about many symbols from the same assembly/module we can answer the
/// question quickly after computing for the first one. Created on demand.
/// </summary>
private ConditionalWeakTable<ISymbol, OriginatingProjectInfo?>? _unrootedSymbolToProjectId;
private static readonly Func<ConditionalWeakTable<ISymbol, OriginatingProjectInfo?>> s_createTable = () => new ConditionalWeakTable<ISymbol, OriginatingProjectInfo?>();
private readonly AsyncLazy<SolutionCompilationState> _cachedFrozenSnapshot;
private SolutionCompilationState(
SolutionState solution,
bool partialSemanticsEnabled,
ImmutableSegmentedDictionary<ProjectId, ICompilationTracker> projectIdToTrackerMap,
SourceGeneratorExecutionVersionMap sourceGeneratorExecutionVersionMap,
TextDocumentStates<SourceGeneratedDocumentState>? frozenSourceGeneratedDocumentStates,
AsyncLazy<SolutionCompilationState>? cachedFrozenSnapshot = null)
{
SolutionState = solution;
PartialSemanticsEnabled = partialSemanticsEnabled;
_projectIdToTrackerMap = projectIdToTrackerMap;
SourceGeneratorExecutionVersionMap = sourceGeneratorExecutionVersionMap;
FrozenSourceGeneratedDocumentStates = frozenSourceGeneratedDocumentStates;
// when solution state is changed, we recalculate its checksum
_lazyChecksums = AsyncLazy.Create(static async (self, cancellationToken) =>
{
var (checksums, projectCone) = await self.ComputeChecksumsAsync(projectId: null, cancellationToken).ConfigureAwait(false);
Contract.ThrowIfTrue(projectCone != null);
return checksums;
}, arg: this);
_cachedFrozenSnapshot = cachedFrozenSnapshot ??
AsyncLazy.Create(synchronousComputeFunction: static (self, c) =>
self.ComputeFrozenSnapshot(c),
arg: this);
CheckInvariants();
}
public SolutionCompilationState(
SolutionState solution,
bool partialSemanticsEnabled)
: this(
solution,
partialSemanticsEnabled,
projectIdToTrackerMap: ImmutableSegmentedDictionary<ProjectId, ICompilationTracker>.Empty,
sourceGeneratorExecutionVersionMap: SourceGeneratorExecutionVersionMap.Empty,
frozenSourceGeneratedDocumentStates: null)
{
}
public SolutionServices Services => this.SolutionState.Services;
// Only run this in debug builds; even the .Any() call across all projects can be expensive when there's a lot of them.
[Conditional("DEBUG")]
private void CheckInvariants()
{
// An id shouldn't point at a tracker for a different project.
Contract.ThrowIfTrue(_projectIdToTrackerMap.Any(kvp => kvp.Key != kvp.Value.ProjectState.Id));
// Solution and SG version maps must correspond to the same set of projects.
Contract.ThrowIfFalse(this.SolutionState.ProjectStates
.Select(kvp => kvp.Key)
.SetEquals(SourceGeneratorExecutionVersionMap.Map.Keys));
}
private SolutionCompilationState Branch(
SolutionState newSolutionState,
ImmutableSegmentedDictionary<ProjectId, ICompilationTracker>? projectIdToTrackerMap = null,
SourceGeneratorExecutionVersionMap? sourceGeneratorExecutionVersionMap = null,
Optional<TextDocumentStates<SourceGeneratedDocumentState>?> frozenSourceGeneratedDocumentStates = default,
AsyncLazy<SolutionCompilationState>? cachedFrozenSnapshot = null)
{
projectIdToTrackerMap ??= _projectIdToTrackerMap;
sourceGeneratorExecutionVersionMap ??= SourceGeneratorExecutionVersionMap;
var newFrozenSourceGeneratedDocumentStates = frozenSourceGeneratedDocumentStates.HasValue ? frozenSourceGeneratedDocumentStates.Value : FrozenSourceGeneratedDocumentStates;
if (newSolutionState == this.SolutionState &&
projectIdToTrackerMap == _projectIdToTrackerMap &&
sourceGeneratorExecutionVersionMap == SourceGeneratorExecutionVersionMap &&
Equals(newFrozenSourceGeneratedDocumentStates, FrozenSourceGeneratedDocumentStates))
{
return this;
}
return new SolutionCompilationState(
newSolutionState,
PartialSemanticsEnabled,
projectIdToTrackerMap.Value,
sourceGeneratorExecutionVersionMap,
newFrozenSourceGeneratedDocumentStates,
cachedFrozenSnapshot);
}
/// <inheritdoc cref="SolutionState.ForkProject"/>
private SolutionCompilationState ForkProject(
StateChange stateChange,
Func<StateChange, TranslationAction?>? translate,
bool forkTracker)
{
return ForkProject(
stateChange,
translate: static (stateChange, translate) => translate?.Invoke(stateChange),
forkTracker,
arg: translate);
}
/// <inheritdoc cref="SolutionState.ForkProject"/>
private SolutionCompilationState ForkProject<TArg>(
StateChange stateChange,
Func<StateChange, TArg, TranslationAction?> translate,
bool forkTracker,
TArg arg)
{
// If the solution didn't actually change, there's no need to change us.
if (stateChange.NewSolutionState == this.SolutionState)
return this;
return ForceForkProject(stateChange, translate.Invoke(stateChange, arg), forkTracker);
}
/// <summary>
/// Same as <see cref="ForkProject{TArg}(StateChange, Func{StateChange, TArg, TranslationAction?}, bool, TArg)"/>
/// except that it will still fork even if newSolutionState is unchanged from <see cref="SolutionState"/>.
/// </summary>
private SolutionCompilationState ForceForkProject(
StateChange stateChange,
TranslationAction? translate,
bool forkTracker)
{
var newSolutionState = stateChange.NewSolutionState;
var newProjectState = stateChange.NewProjectState;
var projectId = newProjectState.Id;
var newDependencyGraph = newSolutionState.GetProjectDependencyGraph();
var newTrackerMap = CreateCompilationTrackerMap(
projectId,
newDependencyGraph,
static (trackerMap, arg) =>
{
// 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 (trackerMap.TryGetValue(arg.projectId, out var tracker))
{
if (!arg.forkTracker)
trackerMap.Remove(arg.projectId);
else
trackerMap[arg.projectId] = tracker.Fork(arg.newProjectState, arg.translate);
}
},
(translate, forkTracker, projectId, newProjectState),
skipEmptyCallback: true);
return this.Branch(
newSolutionState,
projectIdToTrackerMap: newTrackerMap);
}
/// <summary>
/// Creates a mapping of <see cref="ProjectId"/> to <see cref="ICompilationTracker"/>
/// </summary>
/// <param name="changedProjectId">Changed project id</param>
/// <param name="dependencyGraph">Dependency graph</param>
/// <param name="modifyNewTrackerInfo">Callback to modify tracker information. Return value indicates whether the collection was modified.</param>
/// <param name="arg">Data to pass to <paramref name="modifyNewTrackerInfo"/></param>
private ImmutableSegmentedDictionary<ProjectId, ICompilationTracker> CreateCompilationTrackerMap<TArg>(
ProjectId changedProjectId,
ProjectDependencyGraph dependencyGraph,
Action<ImmutableSegmentedDictionary<ProjectId, ICompilationTracker>.Builder, TArg> modifyNewTrackerInfo,
TArg arg,
bool skipEmptyCallback)
{
return CreateCompilationTrackerMap(CanReuse, (changedProjectId, dependencyGraph), modifyNewTrackerInfo, arg, skipEmptyCallback);
// Returns true if 'tracker' can be reused for project 'id'
static bool CanReuse(ProjectId id, (ProjectId changedProjectId, ProjectDependencyGraph dependencyGraph) arg)
{
if (id == arg.changedProjectId)
{
return true;
}
return !arg.dependencyGraph.DoesProjectTransitivelyDependOnProject(id, arg.changedProjectId);
}
}
/// <summary>
/// Creates a mapping of <see cref="ProjectId"/> to <see cref="ICompilationTracker"/>
/// </summary>
/// <param name="changedProjectIds">Changed project ids</param>
/// <param name="dependencyGraph">Dependency graph</param>
/// <param name="modifyNewTrackerInfo">Callback to modify tracker information. Return value indicates whether the collection was modified.</param>
/// <param name="arg">Data to pass to <paramref name="modifyNewTrackerInfo"/></param>
private ImmutableSegmentedDictionary<ProjectId, ICompilationTracker> CreateCompilationTrackerMap<TArg>(
ImmutableArray<ProjectId> changedProjectIds,
ProjectDependencyGraph dependencyGraph,
Action<ImmutableSegmentedDictionary<ProjectId, ICompilationTracker>.Builder, TArg> modifyNewTrackerInfo,
TArg arg,
bool skipEmptyCallback)
{
return CreateCompilationTrackerMap(CanReuse, (changedProjectIds, dependencyGraph), modifyNewTrackerInfo, arg, skipEmptyCallback);
// Returns true if 'tracker' can be reused for project 'id'
static bool CanReuse(ProjectId id, (ImmutableArray<ProjectId> changedProjectIds, ProjectDependencyGraph dependencyGraph) arg)
{
if (arg.changedProjectIds.Contains(id))
return true;
foreach (var changedProjectId in arg.changedProjectIds)
{
if (arg.dependencyGraph.DoesProjectTransitivelyDependOnProject(id, changedProjectId))
return false;
}
return true;
}
}
/// <summary>
/// Creates a mapping of <see cref="ProjectId"/> to <see cref="ICompilationTracker"/>
/// </summary>
/// <param name="canReuse">Callback to determine whether an item can be reused</param>
/// <param name="argCanReuse">Data to pass to <paramref name="argCanReuse"/></param>
/// <param name="modifyNewTrackerInfo">Callback to modify tracker information. Return value indicates whether the collection was modified.</param>
/// <param name="argModifyNewTrackerInfo">Data to pass to <paramref name="modifyNewTrackerInfo"/></param>
private ImmutableSegmentedDictionary<ProjectId, ICompilationTracker> CreateCompilationTrackerMap<TArgCanReuse, TArgModifyNewTrackerInfo>(
Func<ProjectId, TArgCanReuse, bool> canReuse,
TArgCanReuse argCanReuse,
Action<ImmutableSegmentedDictionary<ProjectId, ICompilationTracker>.Builder, TArgModifyNewTrackerInfo> modifyNewTrackerInfo,
TArgModifyNewTrackerInfo argModifyNewTrackerInfo,
bool skipEmptyCallback)
{
// Keep _projectIdToTrackerMap in a local as it can change during the execution of this method
var projectIdToTrackerMap = _projectIdToTrackerMap;
// Avoid allocating the builder if the map is empty and the callback doesn't need
// to be called with empty collections.
if (projectIdToTrackerMap.Count == 0 && skipEmptyCallback)
return projectIdToTrackerMap;
var projectIdToTrackerMapBuilder = projectIdToTrackerMap.ToBuilder();
foreach (var (id, tracker) in projectIdToTrackerMap)
{
if (!canReuse(id, argCanReuse))
{
var localTracker = tracker.Fork(tracker.ProjectState, translate: null);
projectIdToTrackerMapBuilder[id] = localTracker;
}
}
modifyNewTrackerInfo(projectIdToTrackerMapBuilder, argModifyNewTrackerInfo);
return projectIdToTrackerMapBuilder.ToImmutable();
}
/// <summary>
/// Map from each project to the <see cref="SourceGeneratorExecutionVersion"/> it is currently at. Loosely, the
/// execution version allows us to have the generated documents for a project get fixed at some point in the past
/// when they were generated, up until events happen in the host that cause a need for them to be brought up to
/// date. This is ambient, compilation-level, information about our projects, which is why it is stored at this
/// compilation-state level. When syncing to our OOP process, this information is included, allowing the oop side
/// to move its own generators forward when a host changes these versions.
/// </summary>
/// <remarks>
/// Contains information for all projects, even non-C#/VB ones. Though this will have no meaning for those project
/// types.
/// </remarks>
public SourceGeneratorExecutionVersionMap SourceGeneratorExecutionVersionMap { get; }
/// <inheritdoc cref="SolutionState.AddProjects(ArrayBuilder{ProjectInfo})"/>
public SolutionCompilationState AddProjects(ArrayBuilder<ProjectInfo> projectInfos)
{
if (projectInfos.Count == 0)
return this;
var newSolutionState = this.SolutionState.AddProjects(projectInfos);
// When adding a project, we might add a project that an *existing* project now has a reference to. That's
// because we allow existing projects to have 'dangling' project references. As such, we have to ensure we do
// not reuse compilation trackers for any of those projects.
using var _ = PooledHashSet<ProjectId>.GetInstance(out var dependentProjects);
var newDependencyGraph = newSolutionState.GetProjectDependencyGraph();
foreach (var projectInfo in projectInfos)
dependentProjects.AddRange(newDependencyGraph.GetProjectsThatTransitivelyDependOnThisProject(projectInfo.Id));
var newTrackerMap = CreateCompilationTrackerMap(
static (projectId, dependentProjects) => !dependentProjects.Contains(projectId),
dependentProjects,
// We don't need to do anything here. Compilation trackers are created on demand. So we'll just keep the
// tracker map as-is, and have the trackers for these new projects be created when needed.
modifyNewTrackerInfo: static (_, _) => { }, argModifyNewTrackerInfo: default(VoidResult),
skipEmptyCallback: true);
// Add the new projects to the source generator execution version map. Note: it's ok for us to have entries for
// non-C#/VB projects. These will have no effect in-proc as we won't have compilation-trackers for these
// projects. And, when communicating with the OOP process, we'll filter out these projects before sending them
// across in SolutionCompilationState.GetFilteredSourceGenerationExecutionMap.
var versionMapBuilder = SourceGeneratorExecutionVersionMap.Map.ToBuilder();
foreach (var projectInfo in projectInfos)
versionMapBuilder.Add(projectInfo.Id, new());
var sourceGeneratorExecutionVersionMap = new SourceGeneratorExecutionVersionMap(versionMapBuilder.ToImmutable());
return Branch(
newSolutionState,
projectIdToTrackerMap: newTrackerMap,
sourceGeneratorExecutionVersionMap: sourceGeneratorExecutionVersionMap);
}
/// <inheritdoc cref="SolutionState.RemoveProjects"/>
public SolutionCompilationState RemoveProjects(ArrayBuilder<ProjectId> projectIds)
{
if (projectIds.Count == 0)
return this;
// Now go and remove the projects from teh solution-state itself.
var newSolutionState = this.SolutionState.RemoveProjects(projectIds);
var originalDependencyGraph = this.SolutionState.GetProjectDependencyGraph();
using var _ = PooledHashSet<ProjectId>.GetInstance(out var dependentProjects);
// Determine the set of projects that depend on the projects being removed.
foreach (var projectId in projectIds)
{
foreach (var dependentProject in originalDependencyGraph.GetProjectsThatTransitivelyDependOnThisProject(projectId))
dependentProjects.Add(dependentProject);
}
// Now for each compilation tracker.
// 1. remove the compilation tracker if we're removing the project.
// 2. fork teh compilation tracker if it depended on a removed project.
// 3. do nothing for the rest.
var newTrackerMap = CreateCompilationTrackerMap(
// Can reuse the compilation tracker for a project, unless it is some project that had a dependency on one
// of the projects removed.
static (projectId, dependentProjects) => !dependentProjects.Contains(projectId),
dependentProjects,
static (trackerMap, projectIds) =>
{
foreach (var projectId in projectIds)
trackerMap.Remove(projectId);
},
projectIds,
skipEmptyCallback: true);
var versionMapBuilder = SourceGeneratorExecutionVersionMap.Map.ToBuilder();
foreach (var projectId in projectIds)
versionMapBuilder.Remove(projectId);
return this.Branch(
newSolutionState,
projectIdToTrackerMap: newTrackerMap,
sourceGeneratorExecutionVersionMap: new(versionMapBuilder.ToImmutable()));
}
/// <inheritdoc cref="SolutionState.WithProjectAssemblyName"/>
public SolutionCompilationState WithProjectAssemblyName(
ProjectId projectId, string assemblyName)
{
return ForkProject(
this.SolutionState.WithProjectAssemblyName(projectId, assemblyName),
static (stateChange, assemblyName) => new TranslationAction.ProjectAssemblyNameAction(
stateChange.OldProjectState, stateChange.NewProjectState),
forkTracker: true,
arg: assemblyName);
}
/// <inheritdoc cref="SolutionState.WithProjectOutputFilePath"/>
public SolutionCompilationState WithProjectOutputFilePath(ProjectId projectId, string? outputFilePath)
{
return ForkProject(
this.SolutionState.WithProjectOutputFilePath(projectId, outputFilePath),
translate: null,
forkTracker: true);
}
/// <inheritdoc cref="SolutionState.WithProjectOutputRefFilePath"/>
public SolutionCompilationState WithProjectOutputRefFilePath(
ProjectId projectId, string? outputRefFilePath)
{
return ForkProject(
this.SolutionState.WithProjectOutputRefFilePath(projectId, outputRefFilePath),
translate: null,
forkTracker: true);
}
/// <inheritdoc cref="SolutionState.WithProjectCompilationOutputInfo"/>
public SolutionCompilationState WithProjectCompilationOutputInfo(
ProjectId projectId, in CompilationOutputInfo info)
{
return ForkProject(
this.SolutionState.WithProjectCompilationOutputInfo(projectId, info),
translate: null,
forkTracker: true);
}
/// <inheritdoc cref="SolutionState.WithProjectCompilationOutputInfo"/>
public SolutionCompilationState WithProjectDefaultNamespace(
ProjectId projectId, string? defaultNamespace)
{
return ForkProject(
this.SolutionState.WithProjectDefaultNamespace(projectId, defaultNamespace),
translate: null,
forkTracker: true);
}
/// <inheritdoc cref="SolutionState.WithProjectChecksumAlgorithm"/>
public SolutionCompilationState WithProjectChecksumAlgorithm(
ProjectId projectId, SourceHashAlgorithm checksumAlgorithm)
{
return ForkProject(
this.SolutionState.WithProjectChecksumAlgorithm(projectId, checksumAlgorithm),
static stateChange => new TranslationAction.ReplaceAllSyntaxTreesAction(
stateChange.OldProjectState, stateChange.NewProjectState, isParseOptionChange: false),
forkTracker: true);
}
/// <inheritdoc cref="SolutionState.WithProjectName"/>
public SolutionCompilationState WithProjectName(
ProjectId projectId, string name)
{
return ForkProject(
this.SolutionState.WithProjectName(projectId, name),
translate: null,
forkTracker: true);
}
/// <inheritdoc cref="SolutionState.WithProjectFilePath"/>
public SolutionCompilationState WithProjectFilePath(
ProjectId projectId, string? filePath)
{
return ForkProject(
this.SolutionState.WithProjectFilePath(projectId, filePath),
translate: null,
forkTracker: true);
}
/// <inheritdoc cref="SolutionState.WithProjectCompilationOptions"/>
public SolutionCompilationState WithProjectCompilationOptions(
ProjectId projectId, CompilationOptions? options)
{
return ForkProject(
this.SolutionState.WithProjectCompilationOptions(projectId, options),
static stateChange => new TranslationAction.ProjectCompilationOptionsAction(stateChange.OldProjectState, stateChange.NewProjectState),
forkTracker: true);
}
/// <inheritdoc cref="SolutionState.WithProjectParseOptions"/>
public SolutionCompilationState WithProjectParseOptions(
ProjectId projectId, ParseOptions? options)
{
var stateChange = this.SolutionState.WithProjectParseOptions(projectId, options);
if (this.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 ForkProject(
stateChange,
translate: null,
forkTracker: false);
}
else
{
return ForkProject(
stateChange,
static stateChange => new TranslationAction.ReplaceAllSyntaxTreesAction(
stateChange.OldProjectState, stateChange.NewProjectState, isParseOptionChange: true),
forkTracker: true);
}
}
/// <inheritdoc cref="SolutionState.WithHasAllInformation"/>
public SolutionCompilationState WithHasAllInformation(
ProjectId projectId, bool hasAllInformation)
{
return ForkProject(
this.SolutionState.WithHasAllInformation(projectId, hasAllInformation),
translate: null,
forkTracker: true);
}
/// <inheritdoc cref="SolutionState.WithRunAnalyzers"/>
public SolutionCompilationState WithRunAnalyzers(
ProjectId projectId, bool runAnalyzers)
{
return ForkProject(
this.SolutionState.WithRunAnalyzers(projectId, runAnalyzers),
translate: null,
forkTracker: true);
}
/// <inheritdoc cref="SolutionState.WithHasSdkCodeStyleAnalyzers"/>
internal SolutionCompilationState WithHasSdkCodeStyleAnalyzers(
ProjectId projectId, bool hasSdkCodeStyleAnalyzers)
{
return ForkProject(
this.SolutionState.WithHasSdkCodeStyleAnalyzers(projectId, hasSdkCodeStyleAnalyzers),
translate: null,
forkTracker: true);
}
/// <inheritdoc cref="SolutionState.WithProjectDocumentsOrder"/>
public SolutionCompilationState WithProjectDocumentsOrder(
ProjectId projectId, ImmutableList<DocumentId> documentIds)
{
return ForkProject(
this.SolutionState.WithProjectDocumentsOrder(projectId, documentIds),
static stateChange => new TranslationAction.ReplaceAllSyntaxTreesAction(
stateChange.OldProjectState, stateChange.NewProjectState, isParseOptionChange: false),
forkTracker: true);
}
public SolutionCompilationState WithProjectAttributes(ProjectInfo.ProjectAttributes attributes)
{
var projectId = attributes.Id;
var oldProject = SolutionState.GetRequiredProjectState(projectId);
if (oldProject.ProjectInfo.Attributes.Language != attributes.Language)
{
throw new NotSupportedException(WorkspacesResources.Changing_project_language_is_not_supported);
}
if (oldProject.ProjectInfo.Attributes.IsSubmission != attributes.IsSubmission)
{
throw new NotSupportedException(WorkspacesResources.Changing_project_between_ordinary_and_interactive_submission_is_not_supported);
}
return
WithProjectName(projectId, attributes.Name)
.WithProjectAssemblyName(projectId, attributes.AssemblyName)
.WithProjectFilePath(projectId, attributes.FilePath)
.WithProjectOutputFilePath(projectId, attributes.OutputFilePath)
.WithProjectOutputRefFilePath(projectId, attributes.OutputRefFilePath)
.WithProjectCompilationOutputInfo(projectId, attributes.CompilationOutputInfo)
.WithProjectDefaultNamespace(projectId, attributes.DefaultNamespace)
.WithHasAllInformation(projectId, attributes.HasAllInformation)
.WithRunAnalyzers(projectId, attributes.RunAnalyzers)
.WithProjectChecksumAlgorithm(projectId, attributes.ChecksumAlgorithm)
.WithHasSdkCodeStyleAnalyzers(projectId, attributes.HasSdkCodeStyleAnalyzers);
}
public SolutionCompilationState WithProjectInfo(ProjectInfo info)
{
var projectId = info.Id;
var newState = WithProjectAttributes(info.Attributes)
.WithProjectCompilationOptions(projectId, info.CompilationOptions)
.WithProjectParseOptions(projectId, info.ParseOptions)
.WithProjectReferences(projectId, info.ProjectReferences)
.WithProjectMetadataReferences(projectId, info.MetadataReferences)
.WithProjectAnalyzerReferences(projectId, info.AnalyzerReferences);
var oldProjectState = SolutionState.GetRequiredProjectState(projectId);
// Note: buffers are reused across all calls to UpdateDocuments and cleared after each:
using var _1 = ArrayBuilder<DocumentInfo>.GetInstance(out var addedDocumentInfos);
using var _2 = ArrayBuilder<DocumentId>.GetInstance(out var removedDocumentInfos);
UpdateDocuments<DocumentState>(info.Documents);
UpdateDocuments<AdditionalDocumentState>(info.AdditionalDocuments);
UpdateDocuments<AnalyzerConfigDocumentState>(info.AnalyzerConfigDocuments);
return newState;
void UpdateDocuments<TDocumentState>(IReadOnlyList<DocumentInfo> newDocumentInfos)
where TDocumentState : TextDocumentState
{
Debug.Assert(addedDocumentInfos.IsEmpty);
Debug.Assert(removedDocumentInfos.IsEmpty);
using var _3 = ArrayBuilder<TDocumentState>.GetInstance(out var updatedDocuments);
var oldDocumentStates = oldProjectState.GetDocumentStates<TDocumentState>();
foreach (var newDocumentInfo in newDocumentInfos)
{
if (oldDocumentStates.TryGetState(newDocumentInfo.Id, out var oldDocumentState))
{
var newDocumentState = (TDocumentState)oldDocumentState.WithDocumentInfo(newDocumentInfo);
if (oldDocumentState != newDocumentState)
{
updatedDocuments.Add(newDocumentState);
}
}
else
{
addedDocumentInfos.Add(newDocumentInfo);
}
}
if (!oldDocumentStates.Ids.IsEmpty())
{
var newDocumentIdSet = newDocumentInfos.Select(static d => d.Id).ToSet();
foreach (var oldDocumentId in oldDocumentStates.Ids)
{
if (!newDocumentIdSet.Contains(oldDocumentId))
{
removedDocumentInfos.Add(oldDocumentId);
}
}
}
newState = newState
.WithDocumentStatesOfMultipleProjects<TDocumentState>([(projectId, updatedDocuments.ToImmutableAndClear())], GetUpdateDocumentsTranslationAction)
.AddDocumentsToMultipleProjects<TDocumentState>(addedDocumentInfos.ToImmutableAndClear())
.RemoveDocumentsFromSingleProject<TDocumentState>(projectId, removedDocumentInfos.ToImmutableAndClear());
}
}
/// <inheritdoc cref="SolutionState.AddProjectReferences"/>
public SolutionCompilationState AddProjectReferences(
ProjectId projectId, IReadOnlyCollection<ProjectReference> projectReferences)
{
return ForkProject(
this.SolutionState.AddProjectReferences(projectId, projectReferences),
translate: null,
forkTracker: true);
}
/// <inheritdoc cref="SolutionState.RemoveProjectReference"/>
public SolutionCompilationState RemoveProjectReference(ProjectId projectId, ProjectReference projectReference)
{
return ForkProject(
this.SolutionState.RemoveProjectReference(projectId, projectReference),
translate: null,
forkTracker: true);
}
/// <inheritdoc cref="SolutionState.WithProjectReferences"/>
public SolutionCompilationState WithProjectReferences(
ProjectId projectId, IReadOnlyList<ProjectReference> projectReferences)
{
return ForkProject(
this.SolutionState.WithProjectReferences(projectId, projectReferences),
translate: null,
forkTracker: true);
}
/// <inheritdoc cref="SolutionState.AddMetadataReferences"/>
public SolutionCompilationState AddMetadataReferences(
ProjectId projectId, IReadOnlyCollection<MetadataReference> metadataReferences)
{
return ForkProject(
this.SolutionState.AddMetadataReferences(projectId, metadataReferences),
translate: null,
forkTracker: true);
}
/// <inheritdoc cref="SolutionState.RemoveMetadataReference"/>
public SolutionCompilationState RemoveMetadataReference(ProjectId projectId, MetadataReference metadataReference)
{
return ForkProject(
this.SolutionState.RemoveMetadataReference(projectId, metadataReference),
translate: null,
forkTracker: true);
}
/// <inheritdoc cref="SolutionState.WithProjectMetadataReferences"/>
public SolutionCompilationState WithProjectMetadataReferences(
ProjectId projectId, IReadOnlyList<MetadataReference> metadataReferences)
{
return ForkProject(
this.SolutionState.WithProjectMetadataReferences(projectId, metadataReferences),
translate: null,
forkTracker: true);
}
public SolutionCompilationState AddAnalyzerReferences(IReadOnlyCollection<AnalyzerReference> analyzerReferences)
{
// Note: This is the codepath for adding analyzers from vsixes. Importantly, we do not ever get SGs added from
// this codepath, and as such we do not need to update the compilation trackers. The methods that add SGs all
// come from entrypoints that are specific to a particular project.
return Branch(this.SolutionState.AddAnalyzerReferences(analyzerReferences));
}
public SolutionCompilationState RemoveAnalyzerReference(AnalyzerReference analyzerReference)
{
// Note: This is the codepath for removing analyzers from vsixes. Importantly, we do not ever get SGs removed
// from this codepath, and as such we do not need to update the compilation trackers. The methods that remove
// SGs all come from entrypoints that are specific to a particular project.
return Branch(this.SolutionState.RemoveAnalyzerReference(analyzerReference));
}
public SolutionCompilationState WithAnalyzerReferences(IReadOnlyList<AnalyzerReference> analyzerReferences)
{
// Note: This is the codepath for updating analyzers from vsixes. Importantly, we do not ever get SGs changed
// from this codepath, and as such we do not need to update the compilation trackers. The methods that change
// SGs all come from entrypoints that are specific to a particular project.
return Branch(this.SolutionState.WithAnalyzerReferences(analyzerReferences));
}
/// <inheritdoc cref="SolutionState.WithProjectAnalyzerReferences"/>
public SolutionCompilationState WithProjectAnalyzerReferences(
ProjectId projectId, IReadOnlyList<AnalyzerReference> analyzerReferences)
{
return ForkProject(
this.SolutionState.WithProjectAnalyzerReferences(projectId, analyzerReferences),
static stateChange =>
{
// The .Except() methods here aren't going to terribly cheap, but the assumption is adding or removing
// just the generators we changed, rather than creating an entire new generator driver from scratch and
// rerunning all generators, is cheaper in the end. This was written without data backing up that
// assumption, so if a profile indicates to the contrary, this could be changed.
//
// When we're comparing AnalyzerReferences, we'll compare with reference equality; AnalyzerReferences
// like AnalyzerFileReference may implement their own equality, but that can result in things getting
// out of sync: two references that are value equal can still have their own generator instances; it's
// important that as we're adding and removing references that are value equal that we still update with
// the correct generator instances that are coming from the new reference that is actually held in the
// project state from above. An alternative approach would be to call oldProject.WithAnalyzerReferences
// keeping all the references in there that are value equal the same, but this avoids any surprises
// where other components calling WithAnalyzerReferences might not expect that.
var addedReferences = stateChange.NewProjectState.AnalyzerReferences.Except<AnalyzerReference>(stateChange.OldProjectState.AnalyzerReferences, ReferenceEqualityComparer.Instance).ToImmutableArray();
var removedReferences = stateChange.OldProjectState.AnalyzerReferences.Except<AnalyzerReference>(stateChange.NewProjectState.AnalyzerReferences, ReferenceEqualityComparer.Instance).ToImmutableArray();
return new TranslationAction.AddOrRemoveAnalyzerReferencesAction(
stateChange.OldProjectState, stateChange.NewProjectState, referencesToAdd: addedReferences, referencesToRemove: removedReferences);
},
forkTracker: true);
}
/// <inheritdoc cref="SolutionState.WithDocumentAttributes{TValue}"/>
public SolutionCompilationState WithDocumentAttributes<TArg>(
DocumentId documentId,
TArg arg,
Func<DocumentInfo.DocumentAttributes, TArg, DocumentInfo.DocumentAttributes> updateAttributes)
{
return UpdateDocumentState(
SolutionState.WithDocumentAttributes(documentId, arg, updateAttributes), documentId);
}
internal SolutionCompilationState WithDocumentTexts(ImmutableArray<(DocumentId documentId, SourceText text)> texts, PreservationMode mode)
=> UpdateDocumentsInMultipleProjects<DocumentState, SourceText, PreservationMode>(
texts,
arg: mode,
updateDocument: static (oldDocumentState, text, mode) =>
SourceTextIsUnchanged(oldDocumentState, text) ? oldDocumentState : oldDocumentState.UpdateText(text, mode));
private static bool SourceTextIsUnchanged(DocumentState oldDocument, SourceText text)
=> oldDocument.TryGetText(out var oldText) && text == oldText;
/// <summary>
/// Applies an update operation <paramref name="updateDocument"/> to specified <paramref name="documentsToUpdate"/>.
/// Documents may be in different projects.
/// </summary>
private SolutionCompilationState UpdateDocumentsInMultipleProjects<TDocumentState, TDocumentData, TArg>(
ImmutableArray<(DocumentId documentId, TDocumentData documentData)> documentsToUpdate,
TArg arg,
Func<TDocumentState, TDocumentData, TArg, TDocumentState> updateDocument)
where TDocumentState : TextDocumentState
{
return WithDocumentStatesOfMultipleProjects(
documentsToUpdate
.GroupBy(static d => d.documentId.ProjectId)
.Select(g =>
{
var projectId = g.Key;
var oldProjectState = SolutionState.GetRequiredProjectState(projectId);
var oldDocumentStates = oldProjectState.GetDocumentStates<TDocumentState>();
using var _ = ArrayBuilder<TDocumentState>.GetInstance(out var newDocumentStates);
foreach (var (documentId, documentData) in g)
{
var oldDocumentState = oldDocumentStates.GetRequiredState(documentId);
var newDocumentState = updateDocument(oldDocumentState, documentData, arg);
if (ReferenceEquals(oldDocumentState, newDocumentState))
continue;
newDocumentStates.Add(newDocumentState);
}
return (projectId, newDocumentStates.ToImmutableAndClear());
}),
GetUpdateDocumentsTranslationAction);
}
/// <summary>
/// Returns <see cref="SolutionCompilationState"/> with projects updated to new document states specified in <paramref name="updatedDocumentStatesPerProject"/>.
/// </summary>
private SolutionCompilationState WithDocumentStatesOfMultipleProjects<TDocumentState>(
IEnumerable<(ProjectId projectId, ImmutableArray<TDocumentState> updatedDocumentState)> updatedDocumentStatesPerProject,
Func<ProjectState, ImmutableArray<TDocumentState>, TranslationAction> getTranslationAction)
where TDocumentState : TextDocumentState
{
var newCompilationState = this;
foreach (var (projectId, newDocumentStates) in updatedDocumentStatesPerProject)
{
if (newDocumentStates.IsEmpty)
{
continue;
}
var oldProjectState = newCompilationState.SolutionState.GetRequiredProjectState(projectId);
var compilationTranslationAction = getTranslationAction(oldProjectState, newDocumentStates);
var newProjectState = compilationTranslationAction.NewProjectState;
var stateChange = newCompilationState.SolutionState.ForkProject(
oldProjectState,
newProjectState);
newCompilationState = newCompilationState.ForkProject(
stateChange,
static (_, compilationTranslationAction) => compilationTranslationAction,
forkTracker: true,
arg: compilationTranslationAction);
}
return newCompilationState;
}
/// <summary>
/// Updates the <paramref name="oldProjectState"/> to a new state with <paramref name="newDocumentStates"/> and returns a <see cref="TranslationAction"/> that
/// reflects these changes in the project compilation.
/// </summary>
private static TranslationAction GetUpdateDocumentsTranslationAction<TDocumentState>(ProjectState oldProjectState, ImmutableArray<TDocumentState> newDocumentStates)
where TDocumentState : TextDocumentState
{
return newDocumentStates switch
{
ImmutableArray<DocumentState> ordinaryNewDocumentStates => GetUpdateOrdinaryDocumentsTranslationAction(oldProjectState, ordinaryNewDocumentStates),
ImmutableArray<AdditionalDocumentState> additionalNewDocumentStates => GetUpdateAdditionalDocumentsTranslationAction(oldProjectState, additionalNewDocumentStates),
ImmutableArray<AnalyzerConfigDocumentState> analyzerConfigNewDocumentStates => GetUpdateAnalyzerConfigDocumentsTranslationAction(oldProjectState, analyzerConfigNewDocumentStates),
_ => throw ExceptionUtilities.UnexpectedValue(typeof(TDocumentState))
};
TranslationAction GetUpdateOrdinaryDocumentsTranslationAction(ProjectState oldProjectState, ImmutableArray<DocumentState> newDocumentStates)
{
var oldDocumentStates = newDocumentStates.SelectAsArray(static (s, oldProjectState) => oldProjectState.DocumentStates.GetRequiredState(s.Id), oldProjectState);
var newProjectState = oldProjectState.UpdateDocuments(oldDocumentStates, newDocumentStates);
return new TranslationAction.TouchDocumentsAction(oldProjectState, newProjectState, oldDocumentStates, newDocumentStates);
}
TranslationAction GetUpdateAdditionalDocumentsTranslationAction(ProjectState oldProjectState, ImmutableArray<AdditionalDocumentState> newDocumentStates)
{
var oldDocumentStates = newDocumentStates.SelectAsArray(static (s, oldProjectState) => oldProjectState.AdditionalDocumentStates.GetRequiredState(s.Id), oldProjectState);
var newProjectState = oldProjectState.UpdateAdditionalDocuments(oldDocumentStates, newDocumentStates);
return new TranslationAction.TouchAdditionalDocumentsAction(oldProjectState, newProjectState, oldDocumentStates, newDocumentStates);
}
TranslationAction GetUpdateAnalyzerConfigDocumentsTranslationAction(ProjectState oldProjectState, ImmutableArray<AnalyzerConfigDocumentState> newDocumentStates)
{
var oldDocumentStates = newDocumentStates.SelectAsArray(static (s, oldProjectState) => oldProjectState.AnalyzerConfigDocumentStates.GetRequiredState(s.Id), oldProjectState);
var newProjectState = oldProjectState.UpdateAnalyzerConfigDocuments(oldDocumentStates, newDocumentStates);
return new TranslationAction.TouchAnalyzerConfigDocumentsAction(oldProjectState, newProjectState);
}
}
public SolutionCompilationState WithDocumentState(
DocumentState documentState)
{
return UpdateDocumentState(
this.SolutionState.WithDocumentState(documentState), documentState.Id);
}
/// <inheritdoc cref="SolutionState.WithAdditionalDocumentText(DocumentId, SourceText, PreservationMode)"/>
public SolutionCompilationState WithAdditionalDocumentText(
DocumentId documentId, SourceText text, PreservationMode mode)
{
return UpdateAdditionalDocumentState(
this.SolutionState.WithAdditionalDocumentText(documentId, text, mode), documentId);
}
/// <inheritdoc cref="SolutionState.WithAnalyzerConfigDocumentText(DocumentId, SourceText, PreservationMode)"/>
public SolutionCompilationState WithAnalyzerConfigDocumentText(
DocumentId documentId, SourceText text, PreservationMode mode)
{
return UpdateAnalyzerConfigDocumentState(this.SolutionState.WithAnalyzerConfigDocumentText(documentId, text, mode));
}
/// <inheritdoc cref="SolutionState.WithFallbackAnalyzerOptions(ImmutableDictionary{string, StructuredAnalyzerConfigOptions})"/>
public SolutionCompilationState WithFallbackAnalyzerOptions(ImmutableDictionary<string, StructuredAnalyzerConfigOptions> options)
=> Branch(SolutionState.WithFallbackAnalyzerOptions(options));
/// <inheritdoc cref="SolutionState.WithDocumentText(DocumentId, TextAndVersion, PreservationMode)"/>
public SolutionCompilationState WithDocumentText(
DocumentId documentId, TextAndVersion textAndVersion, PreservationMode mode)
{
return UpdateDocumentState(
this.SolutionState.WithDocumentText(documentId, textAndVersion, mode), documentId);
}
/// <inheritdoc cref="SolutionState.WithAdditionalDocumentText(DocumentId, TextAndVersion, PreservationMode)"/>
public SolutionCompilationState WithAdditionalDocumentText(
DocumentId documentId, TextAndVersion textAndVersion, PreservationMode mode)
{
return UpdateAdditionalDocumentState(
this.SolutionState.WithAdditionalDocumentText(documentId, textAndVersion, mode), documentId);
}
/// <inheritdoc cref="SolutionState.WithAnalyzerConfigDocumentText(DocumentId, TextAndVersion, PreservationMode)"/>
public SolutionCompilationState WithAnalyzerConfigDocumentText(
DocumentId documentId, TextAndVersion textAndVersion, PreservationMode mode)
{
return UpdateAnalyzerConfigDocumentState(
this.SolutionState.WithAnalyzerConfigDocumentText(documentId, textAndVersion, mode));
}
/// <inheritdoc cref="Solution.WithDocumentSyntaxRoots(ImmutableArray{ValueTuple{DocumentId, SyntaxNode}}, PreservationMode)"/>
public SolutionCompilationState WithDocumentSyntaxRoots(ImmutableArray<(DocumentId documentId, SyntaxNode root)> syntaxRoots, PreservationMode mode)
{
return UpdateDocumentsInMultipleProjects<DocumentState, SyntaxNode, PreservationMode>(
syntaxRoots,
arg: mode,
static (oldDocumentState, root, mode) =>
oldDocumentState.TryGetSyntaxTree(out var oldTree) && oldTree.TryGetRoot(out var oldRoot) && oldRoot == root
? oldDocumentState
: oldDocumentState.UpdateTree(root, mode));
}
public SolutionCompilationState WithDocumentContentsFrom(
ImmutableArray<(DocumentId documentId, DocumentState documentState)> documentIdsAndStates, bool forceEvenIfTreesWouldDiffer)
{
return UpdateDocumentsInMultipleProjects<DocumentState, DocumentState, bool>(
documentIdsAndStates,
arg: forceEvenIfTreesWouldDiffer,
static (oldDocumentState, documentState, forceEvenIfTreesWouldDiffer) =>
oldDocumentState.TextAndVersionSource == documentState.TextAndVersionSource && oldDocumentState.TreeSource == documentState.TreeSource
? oldDocumentState
: oldDocumentState.UpdateTextAndTreeContents(documentState.TextAndVersionSource, documentState.TreeSource, forceEvenIfTreesWouldDiffer));
}
/// <inheritdoc cref="SolutionState.WithDocumentSourceCodeKind"/>
public SolutionCompilationState WithDocumentSourceCodeKind(
DocumentId documentId, SourceCodeKind sourceCodeKind)
{
return UpdateDocumentState(
this.SolutionState.WithDocumentSourceCodeKind(documentId, sourceCodeKind), documentId);
}
/// <inheritdoc cref="SolutionState.UpdateDocumentTextLoader"/>
public SolutionCompilationState UpdateDocumentTextLoader(
DocumentId documentId, TextLoader loader, PreservationMode mode)
{
var stateChange = this.SolutionState.UpdateDocumentTextLoader(documentId, loader, mode);
// Note: state is currently not reused.
// If UpdateDocumentTextLoader is changed to reuse the state replace this assert with Solution instance reusal.
Debug.Assert(stateChange.NewSolutionState != this.SolutionState);
// Assumes that content has changed. User could have closed a doc without saving and we are loading text
// from closed file with old content.
return UpdateDocumentState(stateChange, documentId);
}
/// <inheritdoc cref="SolutionState.UpdateAdditionalDocumentTextLoader"/>
public SolutionCompilationState UpdateAdditionalDocumentTextLoader(
DocumentId documentId, TextLoader loader, PreservationMode mode)
{
var stateChange = this.SolutionState.UpdateAdditionalDocumentTextLoader(documentId, loader, mode);
// Note: state is currently not reused.
// If UpdateAdditionalDocumentTextLoader is changed to reuse the state replace this assert with Solution instance reusal.
Debug.Assert(stateChange.NewSolutionState != this.SolutionState);
// Assumes that content has changed. User could have closed a doc without saving and we are loading text
// from closed file with old content.
return UpdateAdditionalDocumentState(stateChange, documentId);
}
/// <inheritdoc cref="SolutionState.UpdateAnalyzerConfigDocumentTextLoader"/>
public SolutionCompilationState UpdateAnalyzerConfigDocumentTextLoader(
DocumentId documentId, TextLoader loader, PreservationMode mode)
{
var stateChange = this.SolutionState.UpdateAnalyzerConfigDocumentTextLoader(documentId, loader, mode);
// Note: state is currently not reused.
// If UpdateAnalyzerConfigDocumentTextLoader is changed to reuse the state replace this assert with Solution instance reusal.
Debug.Assert(stateChange.NewSolutionState != this.SolutionState);
// 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 UpdateAnalyzerConfigDocumentState(stateChange);
}
private SolutionCompilationState UpdateDocumentState(StateChange stateChange, DocumentId documentId)
{
return ForkProject(
stateChange,
static (stateChange, documentId) =>
{
// This function shouldn't have been called if the document has not changed
Debug.Assert(stateChange.OldProjectState != stateChange.NewProjectState);
var oldDocument = stateChange.OldProjectState.DocumentStates.GetRequiredState(documentId);
var newDocument = stateChange.NewProjectState.DocumentStates.GetRequiredState(documentId);
return new TranslationAction.TouchDocumentsAction(
stateChange.OldProjectState, stateChange.NewProjectState, [oldDocument], [newDocument]);
},
forkTracker: true,
arg: documentId);
}
private SolutionCompilationState UpdateAdditionalDocumentState(StateChange stateChange, DocumentId documentId)
{
return ForkProject(
stateChange,
static (stateChange, documentId) =>
{
// This function shouldn't have been called if the document has not changed
Debug.Assert(stateChange.OldProjectState != stateChange.NewProjectState);
var oldDocument = stateChange.OldProjectState.AdditionalDocumentStates.GetRequiredState(documentId);
var newDocument = stateChange.NewProjectState.AdditionalDocumentStates.GetRequiredState(documentId);
return new TranslationAction.TouchAdditionalDocumentsAction(
stateChange.OldProjectState, stateChange.NewProjectState, [oldDocument], [newDocument]);
},
forkTracker: true,
arg: documentId);
}
private SolutionCompilationState UpdateAnalyzerConfigDocumentState(StateChange stateChange)
{
return ForkProject(
stateChange,
static stateChange => new TranslationAction.TouchAnalyzerConfigDocumentsAction(stateChange.OldProjectState, stateChange.NewProjectState),
forkTracker: true);
}
/// <summary>
/// Gets the <see cref="Project"/> associated with an assembly symbol.
/// </summary>
public static ProjectId? GetProjectId(IAssemblySymbol? assemblySymbol)
{
if (assemblySymbol == null)
return null;
s_assemblyOrModuleSymbolToProjectMap.TryGetValue(assemblySymbol, out var id);
return id;
}
private bool TryGetCompilationTracker(ProjectId projectId, [NotNullWhen(returnValue: true)] out ICompilationTracker? tracker)
=> _projectIdToTrackerMap.TryGetValue(projectId, out tracker);
private static readonly Func<ProjectId, SolutionState, RegularCompilationTracker> s_createCompilationTrackerFunction = CreateCompilationTracker;
private static RegularCompilationTracker CreateCompilationTracker(ProjectId projectId, SolutionState solution)
{
var projectState = solution.GetProjectState(projectId);
Contract.ThrowIfNull(projectState);
return new RegularCompilationTracker(projectState);
}
private ICompilationTracker GetCompilationTracker(ProjectId projectId)
{
if (!_projectIdToTrackerMap.TryGetValue(projectId, out var tracker))
{
tracker = RoslynImmutableInterlocked.GetOrAdd(ref _projectIdToTrackerMap, projectId, s_createCompilationTrackerFunction, this.SolutionState);
}
return tracker;
}
public Task<VersionStamp> GetDependentVersionAsync(ProjectId projectId, CancellationToken cancellationToken)
=> this.GetCompilationTracker(projectId).GetDependentVersionAsync(this, cancellationToken);
public Task<VersionStamp> GetDependentSemanticVersionAsync(ProjectId projectId, CancellationToken cancellationToken)
=> this.GetCompilationTracker(projectId).GetDependentSemanticVersionAsync(this, cancellationToken);
public Task<Checksum> GetDependentChecksumAsync(ProjectId projectId, CancellationToken cancellationToken)
=> this.GetCompilationTracker(projectId).GetDependentChecksumAsync(this, cancellationToken);
public bool TryGetCompilation(ProjectId projectId, [NotNullWhen(returnValue: true)] out Compilation? compilation)
{
this.SolutionState.CheckContainsProject(projectId);
compilation = null;
return this.TryGetCompilationTracker(projectId, out var tracker)
&& tracker.TryGetCompilation(out compilation);
}
/// <summary>
/// Returns the compilation for the specified <see cref="ProjectId"/>. Can return <see langword="null"/> when the project
/// does not support compilations.
/// </summary>
/// <remarks>
/// The compilation is guaranteed to have a syntax tree for each document of the project.
/// </remarks>
private Task<Compilation?> GetCompilationAsync(ProjectId projectId, CancellationToken cancellationToken)
{
// TODO: figure out where this is called and why the nullable suppression is required
return GetCompilationAsync(this.SolutionState.GetProjectState(projectId)!, cancellationToken);
}
/// <summary>
/// Returns the compilation for the specified <see cref="ProjectState"/>. Can return <see langword="null"/> when the project
/// does not support compilations.
/// </summary>
/// <remarks>
/// The compilation is guaranteed to have a syntax tree for each document of the project.
/// </remarks>
public Task<Compilation?> GetCompilationAsync(ProjectState project, CancellationToken cancellationToken)
{
return project.SupportsCompilation
? GetCompilationTracker(project.Id).GetCompilationAsync(this, cancellationToken).AsNullable()
: SpecializedTasks.Null<Compilation>();
}
/// <summary>
/// Return reference completeness for the given project and all projects this references.
/// </summary>
public Task<bool> 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;
}
/// <summary>
/// Returns the generated document states for source generated documents.
/// </summary>
public ValueTask<TextDocumentStates<SourceGeneratedDocumentState>> GetSourceGeneratedDocumentStatesAsync(ProjectState project, CancellationToken cancellationToken)
=> GetSourceGeneratedDocumentStatesAsync(project, withFrozenSourceGeneratedDocuments: true, cancellationToken);
/// <inheritdoc cref="GetSourceGeneratedDocumentStatesAsync(ProjectState, CancellationToken)"/>
public ValueTask<TextDocumentStates<SourceGeneratedDocumentState>> GetSourceGeneratedDocumentStatesAsync(
ProjectState project, bool withFrozenSourceGeneratedDocuments, CancellationToken cancellationToken)
{
return project.SupportsCompilation
? GetCompilationTracker(project.Id).GetSourceGeneratedDocumentStatesAsync(this, withFrozenSourceGeneratedDocuments, cancellationToken)
: new(TextDocumentStates<SourceGeneratedDocumentState>.Empty);
}
public ValueTask<ImmutableArray<Diagnostic>> GetSourceGeneratorDiagnosticsAsync(
ProjectState project, CancellationToken cancellationToken)
{
return project.SupportsCompilation
? GetCompilationTracker(project.Id).GetSourceGeneratorDiagnosticsAsync(this, cancellationToken)
: new([]);
}
public ValueTask<GeneratorDriverRunResult?> GetSourceGeneratorRunResultAsync(
ProjectState project, CancellationToken cancellationToken)
{
return project.SupportsCompilation
? GetCompilationTracker(project.Id).GetSourceGeneratorRunResultAsync(this, cancellationToken)
: new();
}
/// <summary>
/// Returns the <see cref="SourceGeneratedDocumentState"/> for a source generated document that has already been generated and observed.
/// </summary>
/// <remarks>
/// This is only safe to call if you already have seen the SyntaxTree or equivalent that indicates the document state has already been
/// generated. This method exists to implement <see cref="Solution.GetDocument(SyntaxTree?)"/> and is best avoided unless you're doing something
/// similarly tricky like that.
/// </remarks>
public SourceGeneratedDocumentState? TryGetSourceGeneratedDocumentStateForAlreadyGeneratedId(
DocumentId documentId)
{
return GetCompilationTracker(documentId.ProjectId).TryGetSourceGeneratedDocumentStateForAlreadyGeneratedId(documentId);
}
/// <summary>
/// Get a metadata reference to this compilation info's compilation with respect to
/// another project. For cross language references produce a skeletal assembly. If the
/// compilation is not available, it is built. If a skeletal assembly reference is
/// needed and does not exist, it is also built.
/// </summary>
private async Task<MetadataReference?> GetMetadataReferenceAsync(
ICompilationTracker tracker, ProjectState fromProject, ProjectReference projectReference, bool includeCrossLanguage, CancellationToken cancellationToken)
{
try
{
// If same language then we can wrap the other project's compilation into a compilation reference
if (tracker.ProjectState.LanguageServices == fromProject.LanguageServices)
{
// otherwise, base it off the compilation by building it first.
var compilation = await tracker.GetCompilationAsync(this, cancellationToken).ConfigureAwait(false);
return compilation.ToMetadataReference(projectReference.Aliases, projectReference.EmbedInteropTypes);
}
if (!includeCrossLanguage)
return null;
// otherwise get a metadata only image reference that is built by emitting the metadata from the
// referenced project's compilation and re-importing it.
using (Logger.LogBlock(FunctionId.Workspace_SkeletonAssembly_GetMetadataOnlyImage, cancellationToken))
{
var properties = new MetadataReferenceProperties(aliases: projectReference.Aliases, embedInteropTypes: projectReference.EmbedInteropTypes);
return await tracker.GetOrBuildSkeletonReferenceAsync(this, properties, cancellationToken).ConfigureAwait(false);
}
}
catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken, ErrorSeverity.Critical))
{
throw ExceptionUtilities.Unreachable();
}
}
/// <summary>
/// Get a metadata reference for the project's compilation. Returns <see langword="null"/> upon failure, which
/// can happen when trying to build a skeleton reference that fails to build.
/// </summary>
public Task<MetadataReference?> GetMetadataReferenceAsync(
ProjectReference projectReference, ProjectState fromProject, bool includeCrossLanguage, 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 GetMetadataReferenceAsync(tracker, fromProject, projectReference, includeCrossLanguage, cancellationToken);
}
catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken, ErrorSeverity.Critical))
{
throw ExceptionUtilities.Unreachable();
}
}
/// <summary>
/// Undoes the operation of <see cref="WithFrozenSourceGeneratedDocuments"/>; any frozen source generated document is allowed
/// to have it's real output again.
/// </summary>
public SolutionCompilationState WithoutFrozenSourceGeneratedDocuments()
{
// If there's nothing frozen, there's nothing to do.
if (FrozenSourceGeneratedDocumentStates == null)
return this;
var projectIdsToUnfreeze = FrozenSourceGeneratedDocumentStates.States.Values
.Select(static state => state.Identity.DocumentId.ProjectId)
.Distinct()
.ToImmutableArray();
// Since we previously froze documents in these projects, we should have a CompilationTracker entry for it, and
// it should be a WithFrozenSourceGeneratedDocumentsCompilationTracker. To undo the operation, we'll just
// restore the original CompilationTracker.
var newTrackerMap = CreateCompilationTrackerMap(
projectIdsToUnfreeze,
this.SolutionState.GetProjectDependencyGraph(),
static (trackerMap, projectIdsToUnfreeze) =>
{
foreach (var projectId in projectIdsToUnfreeze)
{
Contract.ThrowIfFalse(trackerMap.TryGetValue(projectId, out var existingTracker));
// TODO(cyrusn): Is it possible to wrap an underlying tracker with multiple frozen document
// compilation trackers? Should we be unwrapping as much as we can here? Or would that also be bad
// given that we're basing what we want to unfreeze on the FrozenSourceGeneratedDocumentStates,
// which may not represent those inner freezes. Unclear. There may be bugs here.
var replacingItemTracker = (WithFrozenSourceGeneratedDocumentsCompilationTracker)existingTracker;
trackerMap[projectId] = replacingItemTracker.UnderlyingTracker;
}
},
projectIdsToUnfreeze,
skipEmptyCallback: projectIdsToUnfreeze.Length == 0);
// We pass the same solution state, since this change is only a change of the generated documents -- none of the core
// documents or project structure changes in any way.
return this.Branch(
this.SolutionState,
projectIdToTrackerMap: newTrackerMap,
frozenSourceGeneratedDocumentStates: null);
}
/// <summary>
/// Returns a new SolutionState that will always produce a specific output for a generated file. This is used only in the
/// implementation of <see cref="TextExtensions.GetOpenDocumentInCurrentContextWithChanges"/> where if a user has a source
/// generated file open, we need to make sure everything lines up.
/// </summary>
public SolutionCompilationState WithFrozenSourceGeneratedDocuments(
ImmutableArray<(SourceGeneratedDocumentIdentity documentIdentity, DateTime generationDateTime, SourceText sourceText)> documents)
{
// We won't support freezing multiple source generated documents more than once in a chain, simply because we have no need
// to support that; these solutions are created on demand when we need to operate on an open source generated document,
// and so those are always forks off the main solution. There's also a bit of a design question -- does calling this a second time
// leave the existing frozen documents in place, or replace them? It depends on the need, but until then we'll cross that bridge
// if/when we need it.
Contract.ThrowIfFalse(FrozenSourceGeneratedDocumentStates == null, $"We shouldn't be calling {nameof(WithFrozenSourceGeneratedDocuments)} on a solution with frozen source generated documents.");
if (documents.IsEmpty)
return this;
// We'll keep track if every document we're reusing is the exact same as the final generated output we already have
using var _ = ArrayBuilder<SourceGeneratedDocumentState>.GetInstance(documents.Length, out var documentStates);
foreach (var (documentIdentity, generationDateTime, sourceText) in documents)
{
var existingGeneratedState = TryGetSourceGeneratedDocumentStateForAlreadyGeneratedId(documentIdentity.DocumentId);
if (existingGeneratedState != null)
{
var newGeneratedState = existingGeneratedState
.WithText(sourceText)
.WithParseOptions(existingGeneratedState.ParseOptions)
.WithGenerationDateTime(generationDateTime);
// If the content already matched, we can just reuse the existing state, so we don't need to track this one
if (newGeneratedState != existingGeneratedState)
documentStates.Add(newGeneratedState);
}
else
{
// There is no document that we know of yet, so we'll add this back in
var projectState = this.SolutionState.GetRequiredProjectState(documentIdentity.DocumentId.ProjectId);
var newGeneratedState = SourceGeneratedDocumentState.Create(
documentIdentity,
sourceText,
projectState.ParseOptions!,
projectState.LanguageServices,
// Just compute the checksum from the source text passed in.
originalSourceTextChecksum: null,
generationDateTime);
documentStates.Add(newGeneratedState);
}
}
// If every document we looked at matched what we've already generated, we have nothing new to do
if (documentStates.Count == 0)
return this;
var documentStatesByProjectId = documentStates.ToDictionary(static state => state.Id.ProjectId);
var newTrackerMap = CreateCompilationTrackerMap(
[.. documentStatesByProjectId.Keys],
this.SolutionState.GetProjectDependencyGraph(),
static (trackerMap, arg) =>
{
foreach (var (projectId, documentStatesForProject) in arg.documentStatesByProjectId)
{
// We want to create a new snapshot with a new compilation tracker that will do this replacement.
// If we already have an existing tracker we'll just wrap that (so we also are reusing any underlying
// computations). If we don't have one, we'll create one and then wrap it.
if (!trackerMap.TryGetValue(projectId, out var existingTracker))
{
existingTracker = CreateCompilationTracker(projectId, arg.SolutionState);
}
trackerMap[projectId] = new WithFrozenSourceGeneratedDocumentsCompilationTracker(existingTracker, new(documentStatesForProject));
}
},
(documentStatesByProjectId, this.SolutionState),
skipEmptyCallback: false);
// We pass the same solution state, since this change is only a change of the generated documents -- none of the core
// documents or project structure changes in any way.
return this.Branch(
this.SolutionState,
projectIdToTrackerMap: newTrackerMap,
frozenSourceGeneratedDocumentStates: new TextDocumentStates<SourceGeneratedDocumentState>(documentStates));
}
public SolutionCompilationState WithNewWorkspace(string? workspaceKind, int workspaceVersion, SolutionServices services)
{
return this.Branch(
this.SolutionState.WithNewWorkspace(workspaceKind, workspaceVersion, services));
}
public SolutionCompilationState WithOptions(SolutionOptionSet options)
{
return this.Branch(
this.SolutionState.WithOptions(options));
}
/// <summary>
/// Updates entries in our <see cref="SourceGeneratorExecutionVersionMap"/> to the corresponding values in the
/// given <paramref name="sourceGeneratorExecutionVersions"/>. Importantly, <paramref
/// name="sourceGeneratorExecutionVersions"/> must refer to projects in this solution. Projects not mentioned in
/// <paramref name="sourceGeneratorExecutionVersions"/> will not be touched (and they will stay in the map).
/// </summary>
public SolutionCompilationState UpdateSpecificSourceGeneratorExecutionVersions(
SourceGeneratorExecutionVersionMap sourceGeneratorExecutionVersions)
{
var versionMapBuilder = SourceGeneratorExecutionVersionMap.Map.ToBuilder();
var newIdToTrackerMapBuilder = _projectIdToTrackerMap.ToBuilder();
var changed = false;
foreach (var (projectId, sourceGeneratorExecutionVersion) in sourceGeneratorExecutionVersions.Map)
{
var currentExecutionVersion = versionMapBuilder[projectId];
// Nothing to do if already at this version.
if (currentExecutionVersion == sourceGeneratorExecutionVersion)
continue;
changed = true;
versionMapBuilder[projectId] = sourceGeneratorExecutionVersion;
// If we do already have a compilation tracker for this project, then let the tracker know that the source
// generator version has changed. We do this by telling it that it should now create SG docs and skeleton
// references if they're out of date.
if (_projectIdToTrackerMap.TryGetValue(projectId, out var existingTracker))
{
// if the major version has changed then we also want to drop the generator driver so that we're rerun
// generators from scratch.
var forceRegeneration = currentExecutionVersion.MajorVersion != sourceGeneratorExecutionVersion.MajorVersion;
var newTracker = existingTracker.WithCreateCreationPolicy(forceRegeneration);
if (newTracker != existingTracker)
newIdToTrackerMapBuilder[projectId] = newTracker;
}
}
if (!changed)
return this;
return this.Branch(
this.SolutionState,
projectIdToTrackerMap: newIdToTrackerMapBuilder.ToImmutable(),
sourceGeneratorExecutionVersionMap: new(versionMapBuilder.ToImmutable()));
}
public SolutionCompilationState WithFrozenPartialCompilations(CancellationToken cancellationToken)
=> _cachedFrozenSnapshot.GetValue(cancellationToken);
private SolutionCompilationState ComputeFrozenSnapshot(CancellationToken cancellationToken)
{
var (newIdToProjectStateMap, newIdToTrackerMap) = ComputeFrozenSnapshotMaps(cancellationToken);
var dependencyGraph = SolutionState.CreateDependencyGraph(this.SolutionState.ProjectIds, newIdToProjectStateMap);
var newState = this.SolutionState.Branch(
idToProjectStateMap: newIdToProjectStateMap,
dependencyGraph: dependencyGraph);
var newCompilationState = this.Branch(
newState,
newIdToTrackerMap,
// Set the frozen solution to be its own frozen solution. Freezing multiple times is a no-op.
cachedFrozenSnapshot: _cachedFrozenSnapshot);
return newCompilationState;
}
private (ImmutableDictionary<ProjectId, ProjectState>, ImmutableSegmentedDictionary<ProjectId, ICompilationTracker>) ComputeFrozenSnapshotMaps(CancellationToken cancellationToken)
{
// Loop until we have calculated the maps against a set of compilation trackers that hasn't changed during our calculation.
while (true)
{
var originalProjectIdToTrackerMap = _projectIdToTrackerMap;
var newIdToProjectStateMapBuilder = this.SolutionState.ProjectStates.ToBuilder();
var newIdToTrackerMapBuilder = originalProjectIdToTrackerMap.ToBuilder();
// Used to track any new compilation trackers created in the loop. This is done to avoid allocations
// from individually adding to the _projectIdToTrackerMap ImmutableSegmentedDictionary .
var updatedIdToTrackerMapBuilder = originalProjectIdToTrackerMap.ToBuilder();
foreach (var projectId in this.SolutionState.ProjectIds)
{
cancellationToken.ThrowIfCancellationRequested();
// Definitely do nothing for non-C#/VB projects. We have nothing to freeze in that case.
var oldProjectState = this.SolutionState.GetRequiredProjectState(projectId);
if (!oldProjectState.SupportsCompilation)
continue;
if (!originalProjectIdToTrackerMap.TryGetValue(projectId, out var oldTracker))
{
oldTracker = CreateCompilationTracker(projectId, this.SolutionState);
// Collect all compilation trackers that needed to be created
updatedIdToTrackerMapBuilder[projectId] = oldTracker;
}
// Since we're freezing, set both generators and skeletons to not be created. We don't want to take any
// perf hit on either of those at all for our clients.
var newTracker = oldTracker.WithDoNotCreateCreationPolicy();
if (oldTracker == newTracker)
continue;
Contract.ThrowIfFalse(newIdToProjectStateMapBuilder.ContainsKey(projectId));
var newProjectState = newTracker.ProjectState;
newIdToProjectStateMapBuilder[projectId] = newProjectState;
newIdToTrackerMapBuilder[projectId] = newTracker;
}
// Attempt to update _projectIdToTrackerMap to include all the newly created compilation trackers. If another thread has updated
// it since we captured it, then we'll need to loop again to ensure we've operated on the latest compilation trackers.
var updatedIdToTrackerMap = updatedIdToTrackerMapBuilder.ToImmutable();
if (originalProjectIdToTrackerMap == RoslynImmutableInterlocked.InterlockedCompareExchange(ref _projectIdToTrackerMap, updatedIdToTrackerMap, originalProjectIdToTrackerMap))
return (newIdToProjectStateMapBuilder.ToImmutable(), newIdToTrackerMapBuilder.ToImmutable());
}
}
/// <summary>
/// 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.
/// <para/>
/// A compilation for the project containing the specified document id will be guaranteed to exist with at least the
/// syntax tree for the document.
/// <para/>
/// This not intended to be the public API, use Document.WithFrozenPartialSemantics() instead.
/// </summary>
public SolutionCompilationState WithFrozenPartialCompilationIncludingSpecificDocument(
DocumentId documentId, CancellationToken cancellationToken)
{
// in progress solutions are disabled for some testing
if (this.Services.GetService<IWorkspacePartialSolutionsTestHook>()?.IsPartialSolutionDisabled == true)
return this;
var currentCompilationState = this;
var currentDocumentState = this.SolutionState.GetRequiredDocumentState(documentId);
// We want all linked versions of this document to also be present in the frozen solution snapshot (that way
// features like 'completion' can see that there are linked docs and give messages about symbols not being
// available in certain project contexts). We do this in a slightly hacky way for perf though. Specifically,
// instead of parsing *all* the sibling files (which can be expensive, especially for a file linked in many
// projects/tfms), we only parse this single tree. We then use that same tree across all siblings. That's
// technically inaccurate, but we can accept that as the primary purpose of 'frozen partial' is to get a
// snapshot *fast* that is allowed to be *inaccurate*.
//
// Note: this does mean that some *potentially* desirable feature behaviors may not be possible. For example,
// because of this unification, all targets will see the user in the same parsed #if region. That means, if the
// user is in a conditionally-disabled region in the primary target, they will also be in such a region in all
// other targets. This would prevent such a feature from using the information from other targets (perhaps
// where it is not conditionally-disabled) to drive a richer experience here. We consider that acceptable given
// the perf benefit. But we could consider relaxing this in the future.
//
// Note: this is very different from the logic we have in the workspace to 'UnifyLinkedDocumentContents'. In
// that case, we only share trees when completely safe and accurate to do so (for example, where no
// directives are involved). As that is used for the real solution snapshot, it must be correct. The
// frozen-partial snapshot is different as it is a fork that is already allowed to be inaccurate for perf
// reasons (for example, missing trees, or missing references).
//
// The 'forceEvenIfTreesWouldDiffer' flag here allows us to share the doc contents even in the case where
// correctness might be violated.
//
// Note: this forking can still be expensive. It would be nice to do this as one large fork step rather than N
// medium sized ones.
//
// Note: GetRelatedDocumentIds will include `documentId` as well. But that's ok. Calling
// WithDocumentContentsFrom with the current document state no-ops immediately, returning back the same
// compilation state instance. So in the case where there are no linked documents, there is no cost here. And
// there is no additional cost processing the initiating document in this loop.
//
// Note: when getting related document ids, we want to include those from different languages. That way we
// ensure a consistent state where all the files (even those shared across languages) agree on their contents.
const bool includeDifferentLanguages = true;
var allDocumentIds = this.SolutionState.GetRelatedDocumentIds(documentId, includeDifferentLanguages);
var allDocumentIdsWithCurrentDocumentState = allDocumentIds.SelectAsArray(static (docId, currentDocumentState) => (docId, currentDocumentState), currentDocumentState);
currentCompilationState = currentCompilationState.WithDocumentContentsFrom(allDocumentIdsWithCurrentDocumentState, forceEvenIfTreesWouldDiffer: true);
return WithFrozenPartialCompilationIncludingSpecificDocumentWorker(currentCompilationState, documentId, cancellationToken);
// Intentionally static, so we only operate on @this, not `this`.
static SolutionCompilationState WithFrozenPartialCompilationIncludingSpecificDocumentWorker(
SolutionCompilationState @this, DocumentId documentId, CancellationToken cancellationToken)
{
try
{
var allDocumentIds = @this.SolutionState.GetRelatedDocumentIds(documentId, includeDifferentLanguages);
using var _ = ArrayBuilder<DocumentState>.GetInstance(allDocumentIds.Length, out var documentStates);
// We grab all the contents of linked files as well to ensure that our snapshot is correct wrt to the
// set of linked document ids our state says are in it. Note: all of these trees should share the same
// green trees, as that is setup in our outer caller. This helps ensure that the cost here is low for
// files with lots of linked siblings.
foreach (var currentDocumentId in allDocumentIds)
{
var documentState = @this.SolutionState.GetRequiredDocumentState(currentDocumentId);
documentStates.Add(documentState);
}
// now freeze the solution state, capturing whatever compilations are in progress.
var frozenCompilationState = @this.WithFrozenPartialCompilations(cancellationToken);
return ComputeFrozenPartialState(frozenCompilationState, documentStates, cancellationToken);
}
catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken, ErrorSeverity.Critical))
{
throw ExceptionUtilities.Unreachable();
}
}
static SolutionCompilationState ComputeFrozenPartialState(
SolutionCompilationState frozenCompilationState,
ArrayBuilder<DocumentState> documentStates,
CancellationToken cancellationToken)
{
var currentState = frozenCompilationState;
using var _ = PooledDictionary<ProjectId, ArrayBuilder<DocumentState>>.GetInstance(out var missingDocumentStates);
// First, either update documents that have changed, or keep track of documents that are missing.
foreach (var newDocumentState in documentStates)
{
var documentId = newDocumentState.Id;
var oldProjectState = currentState.SolutionState.GetRequiredProjectState(documentId.ProjectId);
var oldDocumentState = oldProjectState.DocumentStates.GetState(documentId);
if (oldDocumentState is null)
{
missingDocumentStates.MultiAdd(documentId.ProjectId, newDocumentState);
}
else
{
currentState = currentState.WithDocumentState(newDocumentState);
}
}
// Now, add all missing documents per project.
currentState = currentState.WithDocumentStatesOfMultipleProjects(
// Do a SelectAsArray here to ensure that we realize the array once, and as such only call things like
// ToImmutableAndFree once per ArrayBuilder.
missingDocumentStates.SelectAsArray(kvp => (kvp.Key, kvp.Value.ToImmutableAndFree())),
GetAddDocumentsTranslationAction);
return currentState;
}
}
/// <summary>
/// Core helper that takes a set of <see cref="DocumentInfo" />s and does the application of the appropriate documents to each project.
/// </summary>
/// <param name="documentInfos">The set of documents to add.</param>
public SolutionCompilationState AddDocumentsToMultipleProjects<TDocumentState>(
ImmutableArray<DocumentInfo> documentInfos)
where TDocumentState : 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 one project at a time.
return WithDocumentStatesOfMultipleProjects(
documentInfos.GroupBy(d => d.Id.ProjectId).Select(g =>
{
var projectId = g.Key;
SolutionState.CheckContainsProject(projectId);
var projectState = SolutionState.GetRequiredProjectState(projectId);
return (projectId, newDocumentStates: g.SelectAsArray(projectState.CreateDocument<TDocumentState>));
}),
GetAddDocumentsTranslationAction);
}
public SolutionCompilationState RemoveDocumentsFromMultipleProjects<T>(ImmutableArray<DocumentId> documentIds)
where T : TextDocumentState
{
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 newCompilationState = this;
foreach (var documentIdsInProject in documentIdsByProjectId)
{
newCompilationState = newCompilationState.RemoveDocumentsFromSingleProject<T>(documentIdsInProject.Key, [.. documentIdsInProject]);
}
return newCompilationState;
}
private SolutionCompilationState RemoveDocumentsFromSingleProject<T>(ProjectId projectId, ImmutableArray<DocumentId> documentIds)
where T : TextDocumentState
{
using var _ = ArrayBuilder<T>.GetInstance(out var removedDocumentStates);
var oldProjectState = SolutionState.GetRequiredProjectState(projectId);
var oldDocumentStates = oldProjectState.GetDocumentStates<T>();
foreach (var documentId in documentIds)
{
removedDocumentStates.Add(oldDocumentStates.GetRequiredState(documentId));
}
var removedDocumentStatesForProject = removedDocumentStates.ToImmutable();
var compilationTranslationAction = GetRemoveDocumentsTranslationAction(oldProjectState, documentIds, removedDocumentStatesForProject);
var newProjectState = compilationTranslationAction.NewProjectState;
var stateChange = SolutionState.ForkProject(
oldProjectState,
newProjectState);
return ForkProject(
stateChange,
static (_, compilationTranslationAction) => compilationTranslationAction,
forkTracker: true,
arg: compilationTranslationAction);
}
private static TranslationAction GetRemoveDocumentsTranslationAction<TDocumentState>(ProjectState oldProject, ImmutableArray<DocumentId> documentIds, ImmutableArray<TDocumentState> states)
=> states switch
{
ImmutableArray<DocumentState> documentStates => new TranslationAction.RemoveDocumentsAction(oldProject, oldProject.RemoveDocuments(documentIds), documentStates),
ImmutableArray<AdditionalDocumentState> additionalDocumentStates => new TranslationAction.RemoveAdditionalDocumentsAction(oldProject, oldProject.RemoveAdditionalDocuments(documentIds), additionalDocumentStates),
ImmutableArray<AnalyzerConfigDocumentState> _ => new TranslationAction.TouchAnalyzerConfigDocumentsAction(oldProject, oldProject.RemoveAnalyzerConfigDocuments(documentIds)),
_ => throw ExceptionUtilities.UnexpectedValue(states)
};
private static TranslationAction GetAddDocumentsTranslationAction<TDocumentState>(ProjectState oldProject, ImmutableArray<TDocumentState> states)
=> states switch
{
ImmutableArray<DocumentState> documentStates => new TranslationAction.AddDocumentsAction(oldProject, oldProject.AddDocuments(documentStates), documentStates),
ImmutableArray<AdditionalDocumentState> additionalDocumentStates => new TranslationAction.AddAdditionalDocumentsAction(oldProject, oldProject.AddAdditionalDocuments(additionalDocumentStates), additionalDocumentStates),
ImmutableArray<AnalyzerConfigDocumentState> analyzerConfigDocumentStates => new TranslationAction.TouchAnalyzerConfigDocumentsAction(oldProject, oldProject.AddAnalyzerConfigDocuments(analyzerConfigDocumentStates)),
_ => throw ExceptionUtilities.UnexpectedValue(states)
};
/// <inheritdoc cref="Solution.WithCachedSourceGeneratorState(ProjectId, Project)"/>
public SolutionCompilationState WithCachedSourceGeneratorState(ProjectId projectToUpdate, Project projectWithCachedGeneratorState)
{
this.SolutionState.CheckContainsProject(projectToUpdate);
// First see if we have a generator driver that we can get from the other project.
if (!projectWithCachedGeneratorState.Solution.CompilationState.TryGetCompilationTracker(projectWithCachedGeneratorState.Id, out var tracker) ||
tracker.GeneratorDriver is null)
{
// We don't actually have any state at all, so no change.
return this;
}
var projectToUpdateState = this.SolutionState.GetRequiredProjectState(projectToUpdate);
// Note: we have to force this fork to happen as the actual solution-state object is not changing. We're just
// changing the tracker for a particular project.
var newCompilationState = this.ForceForkProject(
new(this.SolutionState, projectToUpdateState, projectToUpdateState),
translate: new TranslationAction.ReplaceGeneratorDriverAction(
oldProjectState: projectToUpdateState,
newProjectState: projectToUpdateState,
tracker.GeneratorDriver),
forkTracker: true);
return newCompilationState;
}
/// <summary>
/// Creates a new solution instance with all the documents specified updated to have the same specified text.
/// </summary>
public SolutionCompilationState WithDocumentText(IEnumerable<DocumentId?> documentIds, SourceText text, PreservationMode mode)
{
using var _ = ArrayBuilder<(DocumentId, SourceText)>.GetInstance(out var changedDocuments);
foreach (var documentId in documentIds)
{
// This API has always allowed null document IDs and documents IDs not contained within the solution. So
// skip those if we run into that (otherwise the call to WithDocumentText will throw, as it is more
// restrictive).
if (documentId is null)
continue;
var documentState = this.SolutionState.GetProjectState(documentId.ProjectId)?.DocumentStates.GetState(documentId);
if (documentState != null)
{
// before allocating an array below (and calling into a function that does a fair amount of linq work),
// do a fast check if the text has actually changed. this shows up in allocation traces and is
// worthwhile to avoid for the common case where we're continually being asked to update the same doc to
// the same text (for example, when GetOpenDocumentInCurrentContextWithChanges) is called.
//
// The use of GetRequiredState mirrors what happens in WithDocumentTexts
if (!SourceTextIsUnchanged(documentState, text))
changedDocuments.Add((documentId, text));
}
}
if (changedDocuments.Count == 0)
return this;
return this.WithDocumentTexts(changedDocuments.ToImmutableAndClear(), mode);
}
internal TestAccessor GetTestAccessor()
=> new(this);
internal readonly struct TestAccessor(SolutionCompilationState compilationState)
{
public GeneratorDriver? GetGeneratorDriver(Project project)
=> project.SupportsCompilation ? compilationState.GetCompilationTracker(project.Id).GeneratorDriver : null;
}
}
|