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

Merge release/dev17.3 to release/dev17.4 #62961

Merged
merged 3 commits into from
Jul 29, 2022
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 @@ -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