Skip to content

Commit

Permalink
Merge pull request dotnet#62961 from dotnet/merges/release/dev17.3-to…
Browse files Browse the repository at this point in the history
…-release/dev17.4

Merge release/dev17.3 to release/dev17.4
  • Loading branch information
JoeRobich committed Jul 29, 2022
2 parents 7915cb7 + 95a985a commit 5de5410
Show file tree
Hide file tree
Showing 3 changed files with 256 additions and 83 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -182,21 +182,26 @@ public async ValueTask OnCapabilitiesChangedAsync(CancellationToken cancellation

public async ValueTask CommitUpdatesAsync(CancellationToken cancellationToken)
{
if (_disabled)
{
return;
}

var committedDesignTimeSolution = Interlocked.Exchange(ref _pendingUpdatedDesignTimeSolution, null);
Contract.ThrowIfNull(committedDesignTimeSolution);

try
{
var committedDesignTimeSolution = Interlocked.Exchange(ref _pendingUpdatedDesignTimeSolution, null);
Contract.ThrowIfNull(committedDesignTimeSolution);
SolutionCommitted?.Invoke(committedDesignTimeSolution);

_committedDesignTimeSolution = committedDesignTimeSolution;
}
catch (Exception e) when (FatalError.ReportAndCatch(e))
{
}

_committedDesignTimeSolution = committedDesignTimeSolution;

try
{
Contract.ThrowIfTrue(_disabled);
await GetDebuggingSession().CommitSolutionUpdateAsync(_diagnosticService, cancellationToken).ConfigureAwait(false);
}
catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken))
Expand Down Expand Up @@ -260,6 +265,9 @@ private ActiveStatementSpanProvider GetActiveStatementSpanProvider(Solution solu
/// will disappear once the debuggee is resumed - if they are caused by presence of active statements around the change.
/// If the result is a false positive the debugger attempts to apply the changes, which will result in a delay but will correctly end up
/// with no actual deltas to be applied.
///
/// If <paramref name="sourceFilePath"/> is specified checks for changes only in a document of the given path.
/// This is not supported (returns false) for source-generated documents.
/// </summary>
public async ValueTask<bool> HasChangesAsync(string? sourceFilePath, CancellationToken cancellationToken)
{
Expand Down Expand Up @@ -293,9 +301,17 @@ public async ValueTask<ManagedModuleUpdates> GetEditAndContinueUpdatesAsync(Canc
}

var workspace = WorkspaceProvider.Value.Workspace;
var solution = GetCurrentCompileTimeSolution(_pendingUpdatedDesignTimeSolution = workspace.CurrentSolution);
var designTimeSolution = workspace.CurrentSolution;
var solution = GetCurrentCompileTimeSolution(designTimeSolution);
var activeStatementSpanProvider = GetActiveStatementSpanProvider(solution);
var (updates, _, _, _) = await GetDebuggingSession().EmitSolutionUpdateAsync(solution, activeStatementSpanProvider, _diagnosticService, _diagnosticUpdateSource, cancellationToken).ConfigureAwait(false);

// Only store the solution if we have any changes to apply, otherwise CommitUpdatesAsync/DiscardUpdatesAsync won't be called.
if (updates.Status == Contracts.ManagedModuleUpdateStatus.Ready)
{
_pendingUpdatedDesignTimeSolution = designTimeSolution;
}

return updates.FromContract();
}

Expand All @@ -307,9 +323,16 @@ public async ValueTask<ManagedHotReloadUpdates> GetHotReloadUpdatesAsync(Cancell
}

var workspace = WorkspaceProvider.Value.Workspace;
var solution = GetCurrentCompileTimeSolution(_pendingUpdatedDesignTimeSolution = workspace.CurrentSolution);
var designTimeSolution = workspace.CurrentSolution;
var solution = GetCurrentCompileTimeSolution(designTimeSolution);
var (moduleUpdates, diagnosticData, rudeEdits, syntaxError) = await GetDebuggingSession().EmitSolutionUpdateAsync(solution, s_noActiveStatementSpanProvider, _diagnosticService, _diagnosticUpdateSource, cancellationToken).ConfigureAwait(false);

// Only store the solution if we have any changes to apply, otherwise CommitUpdatesAsync/DiscardUpdatesAsync won't be called.
if (moduleUpdates.Status == Contracts.ManagedModuleUpdateStatus.Ready)
{
_pendingUpdatedDesignTimeSolution = designTimeSolution;
}

var updates = moduleUpdates.Updates.SelectAsArray(
update => new ManagedHotReloadUpdate(update.Module, update.ILDelta, update.MetadataDelta, update.PdbDelta, update.UpdatedTypes));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
using System.Reflection.PortableExecutable;
using System.Runtime.Remoting.Contexts;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
Expand All @@ -24,6 +25,7 @@
using Microsoft.CodeAnalysis.Emit;
using Microsoft.CodeAnalysis.ExternalAccess.UnitTesting.Api;
using Microsoft.CodeAnalysis.ExternalAccess.Watch.Api;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.UnitTests;
Expand Down Expand Up @@ -1800,60 +1802,184 @@ public async Task HasChanges()
var (updates, _) = await EmitSolutionUpdateAsync(debuggingSession, solution);
Assert.Equal(ManagedModuleUpdateStatus.Blocked, updates.Status);

// add a document:
// add a project:

oldSolution = solution;
projectC = solution.GetProjectsByName("C").Single();
var documentDId = DocumentId.CreateNewId(projectC.Id);
solution = solution.AddDocument(documentDId, "D", "class D {}", filePath: pathD);
var projectD = solution.AddProject("D", "D", "C#");
solution = projectD.Solution;

Assert.True(await EditSession.HasChangesAsync(oldSolution, solution, CancellationToken.None));
Assert.True(await EditSession.HasChangesAsync(oldSolution, solution, sourceFilePath: pathD, CancellationToken.None));

// remove a document:
// remove a project:
Assert.True(await EditSession.HasChangesAsync(solution, solution.RemoveProject(projectD.Id), CancellationToken.None));

oldSolution = solution;
solution = solution.RemoveDocument(documentDId);
EndDebuggingSession(debuggingSession);
}

Assert.True(await EditSession.HasChangesAsync(oldSolution, solution, CancellationToken.None));
Assert.True(await EditSession.HasChangesAsync(oldSolution, solution, sourceFilePath: pathD, CancellationToken.None));
public enum DocumentKind
{
Source,
Additional,
AnalyzerConfig,
}

// add an additional document:
[Theory]
[CombinatorialData]
public async Task HasChanges_Documents(DocumentKind documentKind)
{
using var _ = CreateWorkspace(out var solution, out var service);

oldSolution = solution;
projectC = solution.GetProjectsByName("C").Single();
var documentXId = DocumentId.CreateNewId(projectC.Id);
solution = solution.AddAdditionalDocument(documentXId, "X", "xxx", filePath: pathX);
var pathX = Path.Combine(TempRoot.Root, "X.cs");
var pathA = Path.Combine(TempRoot.Root, "A.cs");

var generatorExecutionCount = 0;
var generator = new TestSourceGenerator()
{
ExecuteImpl = context =>
{
switch (documentKind)
{
case DocumentKind.Source:
context.AddSource("Generated.cs", context.Compilation.SyntaxTrees.SingleOrDefault(t => t.FilePath.EndsWith("X.cs"))?.ToString() ?? "none");
break;
case DocumentKind.Additional:
context.AddSource("Generated.cs", context.AdditionalFiles.FirstOrDefault()?.GetText().ToString() ?? "none");
break;
case DocumentKind.AnalyzerConfig:
var syntaxTree = context.Compilation.SyntaxTrees.Single(t => t.FilePath.EndsWith("A.cs"));
var content = context.AnalyzerConfigOptions.GetOptions(syntaxTree).TryGetValue("x", out var optionValue) ? optionValue.ToString() : "none";
context.AddSource("Generated.cs", content);
break;
}
generatorExecutionCount++;
}
};

var project = solution.AddProject("A", "A", "C#").AddDocument("A.cs", "", filePath: pathA).Project;
var projectId = project.Id;
solution = project.Solution.AddAnalyzerReference(projectId, new TestGeneratorReference(generator));
project = solution.GetRequiredProject(projectId);
var generatedDocument = (await project.GetSourceGeneratedDocumentsAsync()).Single();
var generatedDocumentId = generatedDocument.Id;

var debuggingSession = await StartDebuggingSessionAsync(service, solution);
EnterBreakState(debuggingSession);

Assert.Equal(1, generatorExecutionCount);
var changedOrAddedDocuments = new PooledObjects.ArrayBuilder<Document>();

//
// Add document
//

generatorExecutionCount = 0;
var oldSolution = solution;
var documentId = DocumentId.CreateNewId(projectId);
solution = documentKind switch
{
DocumentKind.Source => solution.AddDocument(documentId, "X", SourceText.From("xxx", Encoding.UTF8, SourceHashAlgorithm.Sha256), filePath: pathX),
DocumentKind.Additional => solution.AddAdditionalDocument(documentId, "X", SourceText.From("xxx", Encoding.UTF8, SourceHashAlgorithm.Sha256), filePath: pathX),
DocumentKind.AnalyzerConfig => solution.AddAnalyzerConfigDocument(documentId, "X", GetAnalyzerConfigText(new[] { ("x", "1") }), filePath: pathX),
_ => throw ExceptionUtilities.Unreachable,
};
Assert.True(await EditSession.HasChangesAsync(oldSolution, solution, CancellationToken.None));
Assert.True(await EditSession.HasChangesAsync(oldSolution, solution, pathX, CancellationToken.None));

// always returns false for source generated files:
Assert.False(await EditSession.HasChangesAsync(oldSolution, solution, generatedDocument.FilePath, CancellationToken.None));

// generator is not executed since we already know the solution changed without inspecting generated files:
Assert.Equal(0, generatorExecutionCount);

// remove an additional document:
Assert.True(await EditSession.HasChangesAsync(solution, solution.RemoveAdditionalDocument(documentXId), CancellationToken.None));
AssertEx.Equal(new[] { generatedDocumentId },
await EditSession.GetChangedDocumentsAsync(oldSolution.GetProject(projectId), solution.GetProject(projectId), CancellationToken.None).ToImmutableArrayAsync(CancellationToken.None));

// add a config document:
await EditSession.PopulateChangedAndAddedDocumentsAsync(oldSolution.GetProject(projectId), solution.GetProject(projectId), changedOrAddedDocuments, CancellationToken.None);
AssertEx.Equal(documentKind == DocumentKind.Source ? new[] { documentId, generatedDocumentId } : new[] { generatedDocumentId }, changedOrAddedDocuments.Select(d => d.Id));

Assert.Equal(1, generatorExecutionCount);

//
// Update document to a different document snapshot but the same content
//

generatorExecutionCount = 0;
oldSolution = solution;
projectC = solution.GetProjectsByName("C").Single();
var documentYId = DocumentId.CreateNewId(projectC.Id);
solution = solution.AddAnalyzerConfigDocument(documentYId, "Y", SourceText.From("yyy"), filePath: pathY);

Assert.True(await EditSession.HasChangesAsync(oldSolution, solution, CancellationToken.None));
solution = documentKind switch
{
DocumentKind.Source => solution.WithDocumentText(documentId, SourceText.From("xxx", Encoding.UTF8, checksumAlgorithm: SourceHashAlgorithm.Sha256)),
DocumentKind.Additional => solution.WithAdditionalDocumentText(documentId, SourceText.From("xxx", Encoding.UTF8, checksumAlgorithm: SourceHashAlgorithm.Sha256)),
DocumentKind.AnalyzerConfig => solution.WithAnalyzerConfigDocumentText(documentId, GetAnalyzerConfigText(new[] { ("x", "1") })),
_ => throw ExceptionUtilities.Unreachable,
};
Assert.False(await EditSession.HasChangesAsync(oldSolution, solution, CancellationToken.None));
Assert.False(await EditSession.HasChangesAsync(oldSolution, solution, pathX, CancellationToken.None));

// remove a config document:
Assert.True(await EditSession.HasChangesAsync(solution, solution.RemoveAnalyzerConfigDocument(documentYId), CancellationToken.None));
Assert.Equal(0, generatorExecutionCount);

// add a project:
// source generator infrastructure compares content and reuses state if it matches (SourceGeneratedDocumentState.WithUpdatedGeneratedContent):
AssertEx.Equal(documentKind == DocumentKind.Source ? new[] { documentId } : Array.Empty<DocumentId>(),
await EditSession.GetChangedDocumentsAsync(oldSolution.GetProject(projectId), solution.GetProject(projectId), CancellationToken.None).ToImmutableArrayAsync(CancellationToken.None));

await EditSession.PopulateChangedAndAddedDocumentsAsync(oldSolution.GetProject(projectId), solution.GetProject(projectId), changedOrAddedDocuments, CancellationToken.None);
Assert.Empty(changedOrAddedDocuments);

Assert.Equal(1, generatorExecutionCount);

//
// Update document content
//

generatorExecutionCount = 0;
oldSolution = solution;
var projectD = solution.AddProject("D", "D", "C#");
solution = projectD.Solution;
solution = documentKind switch
{
DocumentKind.Source => solution.WithDocumentText(documentId, SourceText.From("xxx-changed", Encoding.UTF8, checksumAlgorithm: SourceHashAlgorithm.Sha256)),
DocumentKind.Additional => solution.WithAdditionalDocumentText(documentId, SourceText.From("xxx-changed", Encoding.UTF8, checksumAlgorithm: SourceHashAlgorithm.Sha256)),
DocumentKind.AnalyzerConfig => solution.WithAnalyzerConfigDocumentText(documentId, GetAnalyzerConfigText(new[] { ("x", "2") })),
_ => throw ExceptionUtilities.Unreachable,
};
Assert.True(await EditSession.HasChangesAsync(oldSolution, solution, CancellationToken.None));
Assert.True(await EditSession.HasChangesAsync(oldSolution, solution, pathX, CancellationToken.None));

AssertEx.Equal(documentKind == DocumentKind.Source ? new[] { documentId, generatedDocumentId } : new[] { generatedDocumentId },
await EditSession.GetChangedDocumentsAsync(oldSolution.GetProject(projectId), solution.GetProject(projectId), CancellationToken.None).ToImmutableArrayAsync(CancellationToken.None));

await EditSession.PopulateChangedAndAddedDocumentsAsync(oldSolution.GetProject(projectId), solution.GetProject(projectId), changedOrAddedDocuments, CancellationToken.None);
AssertEx.Equal(documentKind == DocumentKind.Source ? new[] { documentId, generatedDocumentId } : new[] { generatedDocumentId }, changedOrAddedDocuments.Select(d => d.Id));

Assert.Equal(1, generatorExecutionCount);

//
// Remove document
//

generatorExecutionCount = 0;
oldSolution = solution;
solution = documentKind switch
{
DocumentKind.Source => solution.RemoveDocument(documentId),
DocumentKind.Additional => solution.RemoveAdditionalDocument(documentId),
DocumentKind.AnalyzerConfig => solution.RemoveAnalyzerConfigDocument(documentId),
_ => throw ExceptionUtilities.Unreachable,
};
Assert.True(await EditSession.HasChangesAsync(oldSolution, solution, CancellationToken.None));
Assert.True(await EditSession.HasChangesAsync(oldSolution, solution, pathX, CancellationToken.None));

// remove a project:
Assert.True(await EditSession.HasChangesAsync(solution, solution.RemoveProject(projectD.Id), CancellationToken.None));
Assert.Equal(0, generatorExecutionCount);

EndDebuggingSession(debuggingSession);
AssertEx.Equal(new[] { generatedDocumentId },
await EditSession.GetChangedDocumentsAsync(oldSolution.GetProject(projectId), solution.GetProject(projectId), CancellationToken.None).ToImmutableArrayAsync(CancellationToken.None));

await EditSession.PopulateChangedAndAddedDocumentsAsync(oldSolution.GetProject(projectId), solution.GetProject(projectId), changedOrAddedDocuments, CancellationToken.None);
AssertEx.Equal(new[] { generatedDocumentId }, changedOrAddedDocuments.Select(d => d.Id));

Assert.Equal(1, generatorExecutionCount);
}

[Fact]
Expand Down Expand Up @@ -3901,7 +4027,7 @@ int F()
/// Function remapping is produced for F v1 -> F v2.
/// 2) Hot-reload edit F (without breaking) to version 3.
/// Function remapping is not produced for F v2 -> F v3. If G ever returned to F it will be remapped from F v1 -> F v2,
/// where F v2 is considered stale code. This is consistent with the semantic of Hot Reload: Hot Reloaded changes do not have
/// where F v2 is considered stale code. This is consistent with the semantic of Hot Reload: Hot Reloaded changes do not have
/// an effect until the method is called again. In this case the method is not called, it it returned into hence the stale
/// version executes.
/// 3) Break and apply EnC edit. This edit is to F v3 (Hot Reload) of the method. We will produce remapping F v3 -> v4.
Expand Down
Loading

0 comments on commit 5de5410

Please sign in to comment.