Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix issue where we were trying to freeze non-C#/VB projects #72277

Merged
merged 8 commits into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1064,22 +1064,25 @@ private SolutionCompilationState ComputeFrozenSnapshot(CancellationToken cancell
var newIdToProjectStateMapBuilder = this.SolutionState.ProjectStates.ToBuilder();
var newIdToTrackerMapBuilder = _projectIdToTrackerMap.ToBuilder();

using var _1 = ArrayBuilder<TextDocumentState>.GetInstance(out var documentsToRemove);
using var _2 = ArrayBuilder<TextDocumentState>.GetInstance(out var documentsToAdd);
var filePathToDocumentIdsMapBuilder = this.SolutionState.FilePathToDocumentIdsMap.ToBuilder();
var filePathToDocumentIdsMapChanged = false;

foreach (var projectId in this.SolutionState.ProjectIds)
{
cancellationToken.ThrowIfCancellationRequested();

// if we don't have one or it is stale, create a new partial solution
// 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;

var oldTracker = GetCompilationTracker(projectId);
var newTracker = oldTracker.FreezePartialState(cancellationToken);
if (oldTracker == newTracker)
continue;

Contract.ThrowIfFalse(newIdToProjectStateMapBuilder.ContainsKey(projectId));

var oldProjectState = this.SolutionState.GetRequiredProjectState(projectId);
var newProjectState = newTracker.ProjectState;

newIdToProjectStateMapBuilder[projectId] = newProjectState;
Expand Down Expand Up @@ -1110,9 +1113,10 @@ private SolutionCompilationState ComputeFrozenSnapshot(CancellationToken cancell
var newIdToProjectStateMap = newIdToProjectStateMapBuilder.ToImmutable();
var newIdToTrackerMap = newIdToTrackerMapBuilder.ToImmutable();

var filePathToDocumentIdsMap = this.SolutionState.CreateFilePathToDocumentIdsMapWithAddedAndRemovedDocuments(
documentsToAdd: documentsToAdd,
documentsToRemove: documentsToRemove);
var filePathToDocumentIdsMap = filePathToDocumentIdsMapChanged
? filePathToDocumentIdsMapBuilder.ToImmutable()
: null;

var dependencyGraph = SolutionState.CreateDependencyGraph(this.SolutionState.ProjectIds, newIdToProjectStateMap);

var newState = this.SolutionState.Branch(
Expand All @@ -1132,12 +1136,22 @@ void CheckDocumentStates<TDocumentState>(
TextDocumentStates<TDocumentState> oldStates,
TextDocumentStates<TDocumentState> newStates) where TDocumentState : TextDocumentState
{
if (oldStates.Equals(newStates))
return;

// Get the trivial sets of documents that are present in one set but not the other.

foreach (var documentId in newStates.GetAddedStateIds(oldStates))
documentsToAdd.Add(newStates.GetRequiredState(documentId));
{
filePathToDocumentIdsMapChanged = true;
SolutionState.AddDocumentFilePath(newStates.GetRequiredState(documentId), filePathToDocumentIdsMapBuilder);
}

foreach (var documentId in newStates.GetRemovedStateIds(oldStates))
documentsToRemove.Add(oldStates.GetRequiredState(documentId));
{
filePathToDocumentIdsMapChanged = true;
SolutionState.RemoveDocumentFilePath(oldStates.GetRequiredState(documentId), filePathToDocumentIdsMapBuilder);
}

// Now go through the states that are in both sets. We have to check these all as it is possible for
// document to change its file path without its id changing.
Expand All @@ -1147,8 +1161,9 @@ void CheckDocumentStates<TDocumentState>(
oldDocumentState != newDocumentState &&
oldDocumentState.FilePath != newDocumentState.FilePath)
{
documentsToRemove.Remove(oldDocumentState);
documentsToAdd.Add(newDocumentState);
filePathToDocumentIdsMapChanged = true;
SolutionState.RemoveDocumentFilePath(oldDocumentState, filePathToDocumentIdsMapBuilder);
SolutionState.AddDocumentFilePath(newDocumentState, filePathToDocumentIdsMapBuilder);
}
}
}
Expand Down
60 changes: 21 additions & 39 deletions src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,8 @@ public SolutionState WithNewWorkspace(
_lazyAnalyzers);
}

public ImmutableDictionary<string, ImmutableArray<DocumentId>> FilePathToDocumentIdsMap => _filePathToDocumentIdsMap;

/// <summary>
/// The version of the most recently modified project.
/// </summary>
Expand Down Expand Up @@ -404,25 +406,6 @@ public SolutionState RemoveProject(ProjectId projectId)
dependencyGraph: newDependencyGraph);
}

public ImmutableDictionary<string, ImmutableArray<DocumentId>> CreateFilePathToDocumentIdsMapWithAddedAndRemovedDocuments(
ArrayBuilder<TextDocumentState> documentsToAdd,
ArrayBuilder<TextDocumentState> documentsToRemove)
{
if (documentsToRemove.Count == 0 && documentsToAdd.Count == 0)
return _filePathToDocumentIdsMap;

var builder = _filePathToDocumentIdsMap.ToBuilder();

// Add first, then remove. This helps avoid the case where a filepath now sees no documents, so we remove
// the entry entirely for it in the dictionary, only to add it back in. Adding then removing will at least
// keep the entry, but increase the docs for it, then lower it back down.

AddDocumentFilePaths(documentsToAdd, builder);
RemoveDocumentFilePaths(documentsToRemove, builder);

return builder.ToImmutable();
}

public ImmutableDictionary<string, ImmutableArray<DocumentId>> CreateFilePathToDocumentIdsMapWithAddedDocuments(IEnumerable<TextDocumentState> documentStates)
{
var builder = _filePathToDocumentIdsMap.ToBuilder();
Expand All @@ -433,16 +416,17 @@ public ImmutableDictionary<string, ImmutableArray<DocumentId>> CreateFilePathToD
private static void AddDocumentFilePaths(IEnumerable<TextDocumentState> documentStates, ImmutableDictionary<string, ImmutableArray<DocumentId>>.Builder builder)
{
foreach (var documentState in documentStates)
{
var filePath = documentState.FilePath;
AddDocumentFilePath(documentState, builder);
}

if (RoslynString.IsNullOrEmpty(filePath))
{
continue;
}
public static void AddDocumentFilePath(TextDocumentState documentState, ImmutableDictionary<string, ImmutableArray<DocumentId>>.Builder builder)
{
var filePath = documentState.FilePath;

builder.MultiAdd(filePath, documentState.Id);
}
if (RoslynString.IsNullOrEmpty(filePath))
return;

builder.MultiAdd(filePath, documentState.Id);
}

public ImmutableDictionary<string, ImmutableArray<DocumentId>> CreateFilePathToDocumentIdsMapWithRemovedDocuments(IEnumerable<TextDocumentState> documentStates)
Expand All @@ -455,21 +439,19 @@ public ImmutableDictionary<string, ImmutableArray<DocumentId>> CreateFilePathToD
private static void RemoveDocumentFilePaths(IEnumerable<TextDocumentState> documentStates, ImmutableDictionary<string, ImmutableArray<DocumentId>>.Builder builder)
{
foreach (var documentState in documentStates)
{
var filePath = documentState.FilePath;
RemoveDocumentFilePath(documentState, builder);
}

if (RoslynString.IsNullOrEmpty(filePath))
{
continue;
}
public static void RemoveDocumentFilePath(TextDocumentState documentState, ImmutableDictionary<string, ImmutableArray<DocumentId>>.Builder builder)
{
var filePath = documentState.FilePath;
if (RoslynString.IsNullOrEmpty(filePath))
return;

if (!builder.TryGetValue(filePath, out var documentIdsWithPath) || !documentIdsWithPath.Contains(documentState.Id))
{
throw new ArgumentException($"The given documentId was not found in '{nameof(_filePathToDocumentIdsMap)}'.");
}
if (!builder.TryGetValue(filePath, out var documentIdsWithPath) || !documentIdsWithPath.Contains(documentState.Id))
throw new ArgumentException($"The given documentId was not found in '{nameof(_filePathToDocumentIdsMap)}'.");

builder.MultiRemove(filePath, documentState.Id);
}
builder.MultiRemove(filePath, documentState.Id);
}

private ImmutableDictionary<string, ImmutableArray<DocumentId>> CreateFilePathToDocumentIdsMapWithFilePath(DocumentId documentId, string? oldFilePath, string? newFilePath)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Serialization;
using Roslyn.Utilities;

Expand Down Expand Up @@ -217,6 +216,15 @@ private static IEnumerable<DocumentId> Except(ImmutableList<DocumentId> ids, Imm
public bool HasAnyStateChanges(TextDocumentStates<TState> oldStates)
=> !_map.Values.SequenceEqual(oldStates._map.Values);

public override bool Equals(object? obj)
=> obj is TextDocumentStates<TState> other && Equals(other);

public override int GetHashCode()
=> throw new NotSupportedException();

public bool Equals(TextDocumentStates<TState> other)
=> _map == other._map && _ids == other.Ids;

private sealed class DocumentIdComparer : IComparer<DocumentId?>
{
public static readonly IComparer<DocumentId?> Instance = new DocumentIdComparer();
Expand Down
16 changes: 16 additions & 0 deletions src/Workspaces/CoreTest/SolutionTests/SolutionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4719,5 +4719,21 @@ public async Task TestFrozenPartialSolution7(bool freeze)
Assert.Equal("// source2", forkedGeneratedDocuments.Single().GetTextSynchronously(CancellationToken.None).ToString());
}
}

[Fact]
public async Task TestFrozenPartialSolutionOtherLanguage()
{
using var workspace = WorkspaceTestUtilities.CreateWorkspaceWithPartialSemantics();
var project = workspace.CurrentSolution.AddProject("TypeScript", "TypeScript", "TypeScript");
project = project.AddDocument("Extra.ts", SourceText.From("class Extra { }")).Project;

// Freeze should have no impact on non-c#/vb projects.
var frozenSolution = project.Solution.WithFrozenPartialCompilations(CancellationToken.None);
var frozenProject = frozenSolution.Projects.Single();
Assert.Single(frozenProject.Documents);

var frozenCompilation = await frozenProject.GetCompilationAsync();
Assert.Null(frozenCompilation);
}
}
}
Loading