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

Handle projects added during break state #56266

Merged
merged 2 commits into from
Sep 9, 2021
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 @@ -1603,6 +1603,97 @@ public async Task FileStatus_CompilationError()
EndDebuggingSession(debuggingSession);
}

[Fact]
[WorkItem(1204, "https://github.com/dotnet/roslyn/issues/1204")]
[WorkItem(1371694, "https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1371694")]
public async Task Project_Add()
{
var sourceA1 = "class A { void M() { System.Console.WriteLine(1); } }";
var sourceB1 = "class B { int F() => 1; }";
var sourceB2 = "class B { int G() => 1; }";
var sourceB3 = "class B { int F() => 2; }";

var dir = Temp.CreateDirectory();
var sourceFileA = dir.CreateFile("a.cs").WriteAllText(sourceA1);
var sourceFileB = dir.CreateFile("b.cs").WriteAllText(sourceB1);

using var _ = CreateWorkspace(out var solution, out var service);
solution = AddDefaultTestProject(solution, new[] { sourceA1 });
var documentA1 = solution.Projects.Single().Documents.Single();

var mvidA = EmitAndLoadLibraryToDebuggee(sourceA1, sourceFilePath: sourceFileA.Path, assemblyName: "A");
var mvidB = EmitAndLoadLibraryToDebuggee(sourceB1, sourceFilePath: sourceFileB.Path, assemblyName: "B");

var debuggingSession = await StartDebuggingSessionAsync(service, solution);

// An active statement may be present in the added file since the file exists in the PDB:
var activeLineSpanA1 = SourceText.From(sourceA1, Encoding.UTF8).Lines.GetLinePositionSpan(GetSpan(sourceA1, "System.Console.WriteLine(1);"));
var activeLineSpanB1 = SourceText.From(sourceB1, Encoding.UTF8).Lines.GetLinePositionSpan(GetSpan(sourceB1, "1"));

var activeStatements = ImmutableArray.Create(
new ManagedActiveStatementDebugInfo(
new ManagedInstructionId(new ManagedMethodId(mvidA, token: 0x06000001, version: 1), ilOffset: 1),
sourceFileA.Path,
activeLineSpanA1.ToSourceSpan(),
ActiveStatementFlags.IsLeafFrame | ActiveStatementFlags.MethodUpToDate),
new ManagedActiveStatementDebugInfo(
new ManagedInstructionId(new ManagedMethodId(mvidB, token: 0x06000001, version: 1), ilOffset: 1),
sourceFileB.Path,
activeLineSpanB1.ToSourceSpan(),
ActiveStatementFlags.IsLeafFrame | ActiveStatementFlags.MethodUpToDate));

EnterBreakState(debuggingSession, activeStatements);

// add project that matches assembly B and update the document:

var documentB2 = solution.
AddProject("B", "B", LanguageNames.CSharp).
AddDocument("b.cs", SourceText.From(sourceB2, Encoding.UTF8, SourceHashAlgorithm.Sha256), filePath: sourceFileB.Path);

solution = documentB2.Project.Solution;

// TODO: https://github.com/dotnet/roslyn/issues/1204
// Should return span in document B since the document content matches the PDB.
var baseSpans = await debuggingSession.GetBaseActiveStatementSpansAsync(solution, ImmutableArray.Create(documentA1.Id, documentB2.Id), CancellationToken.None);
AssertEx.Equal(new[]
{
"<empty>",
"(0,21)-(0,22)"
}, baseSpans.Select(spans => spans.IsEmpty ? "<empty>" : string.Join(",", spans.Select(s => s.LineSpan.ToString()))));

var trackedActiveSpans = ImmutableArray.Create(
new ActiveStatementSpan(1, activeLineSpanB1, ActiveStatementFlags.MethodUpToDate | ActiveStatementFlags.IsLeafFrame, unmappedDocumentId: null));

var currentSpans = await debuggingSession.GetAdjustedActiveStatementSpansAsync(documentB2, (_, _, _) => new(trackedActiveSpans), CancellationToken.None);
// TODO: https://github.com/dotnet/roslyn/issues/1204
// AssertEx.Equal(trackedActiveSpans, currentSpans);
Assert.Empty(currentSpans);

Assert.Equal(activeLineSpanB1,
await debuggingSession.GetCurrentActiveStatementPositionAsync(documentB2.Project.Solution, (_, _, _) => new(trackedActiveSpans), activeStatements[1].ActiveInstruction, CancellationToken.None));

var diagnostics = await service.GetDocumentDiagnosticsAsync(documentB2, s_noActiveSpans, CancellationToken.None);

// TODO: https://github.com/dotnet/roslyn/issues/1204
//AssertEx.Equal(
// new[] { "ENC0020: " + string.Format(FeaturesResources.Renaming_0_requires_restarting_the_application, FeaturesResources.method) },
// diagnostics.Select(d => $"{d.Id}: {d.GetMessage()}"));
Assert.Empty(diagnostics);

// update document with a valid change:
solution = solution.WithDocumentText(documentB2.Id, SourceText.From(sourceB3, Encoding.UTF8, SourceHashAlgorithm.Sha256));

var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution);

// TODO: https://github.com/dotnet/roslyn/issues/1204
// verify valid update
Assert.Equal(ManagedModuleUpdateStatus.None, updates.Status);

ExitBreakState(debuggingSession);

EndDebuggingSession(debuggingSession);
}

[Fact]
public async Task Capabilities()
{
Expand Down
13 changes: 13 additions & 0 deletions src/Features/Core/Portable/EditAndContinue/CommittedSolution.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
Expand Down Expand Up @@ -214,6 +215,14 @@ public Task OnSourceFileUpdatedAsync(Document document, CancellationToken cancel
return (null, DocumentState.None);
}

// TODO: Handle case when the old project does not exist and needs to be added. https://github.com/dotnet/roslyn/issues/1204
if (committedDocument == null && !solution.ContainsProject(document.Project.Id))
{
// Document in a new project that does not exist in the committed solution.
// Pretend this document is design-time-only and ignore it.
return (null, DocumentState.DesignTimeOnly);
}

if (!document.DocumentState.SupportsEditAndContinue())
{
return (null, DocumentState.DesignTimeOnly);
Expand Down Expand Up @@ -271,6 +280,10 @@ public Task OnSourceFileUpdatedAsync(Document document, CancellationToken cancel
// Add the document to the committed solution with its current (possibly out-of-sync) text.
if (committedDocument == null)
{
// TODO: Handle case when the old project does not exist and needs to be added. https://github.com/dotnet/roslyn/issues/1204
Debug.Assert(_solution.ContainsProject(documentId.ProjectId));

// TODO: Use API proposed in https://github.com/dotnet/roslyn/issues/56253.
_solution = _solution.AddDocument(DocumentInfo.Create(
documentId,
name: document.Name,
Expand Down
37 changes: 26 additions & 11 deletions src/Features/Core/Portable/EditAndContinue/DebuggingSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,11 @@ internal sealed class DebuggingSession : IDisposable
private PendingSolutionUpdate? _pendingUpdate;
private Action<DebuggingSessionTelemetry.Data> _reportTelemetry;

#pragma warning disable IDE0052 // Remove unread private members
/// <summary>
/// Last array of module updates generated during the debugging session.
/// Useful for crash dump diagnostics.
/// </summary>
private ImmutableArray<ManagedModuleUpdate> _lastModuleUpdatesLog;
#pragma warning restore

internal DebuggingSession(
DebuggingSessionId id,
Expand Down Expand Up @@ -608,10 +606,16 @@ public async ValueTask<ImmutableArray<ImmutableArray<ActiveStatementSpan>>> GetB
// Analyze changed documents in projects containing active statements:
foreach (var projectId in projectIds)
{
var oldProject = LastCommittedSolution.GetProject(projectId);
if (oldProject == null)
{
// document is in a project that's been added to the solution
continue;
}

var newProject = solution.GetRequiredProject(projectId);
var analyzer = newProject.LanguageServices.GetRequiredService<IEditAndContinueAnalyzer>();

await foreach (var documentId in EditSession.GetChangedDocumentsAsync(LastCommittedSolution, newProject, cancellationToken).ConfigureAwait(false))
await foreach (var documentId in EditSession.GetChangedDocumentsAsync(oldProject, newProject, cancellationToken).ConfigureAwait(false))
{
cancellationToken.ThrowIfCancellationRequested();

Expand Down Expand Up @@ -716,7 +720,8 @@ public async ValueTask<ImmutableArray<ActiveStatementSpan>> GetAdjustedActiveSta
var oldProject = LastCommittedSolution.GetProject(newProject.Id);
if (oldProject == null)
{
// project has been added, no changes in active statement spans:
// TODO: https://github.com/dotnet/roslyn/issues/1204
// Enumerate all documents of the new project.
return ImmutableArray<ActiveStatementSpan>.Empty;
}

Expand All @@ -741,7 +746,7 @@ public async ValueTask<ImmutableArray<ActiveStatementSpan>> GetAdjustedActiveSta
adjustedMappedSpans.AddRange(newDocumentActiveStatementSpans);

// Update tracking spans to the latest known locations of the active statements contained in changed documents based on their analysis.
await foreach (var unmappedDocumentId in EditSession.GetChangedDocumentsAsync(LastCommittedSolution, newProject, cancellationToken).ConfigureAwait(false))
await foreach (var unmappedDocumentId in EditSession.GetChangedDocumentsAsync(oldProject, newProject, cancellationToken).ConfigureAwait(false))
{
var newUnmappedDocument = await newSolution.GetRequiredDocumentAsync(unmappedDocumentId, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false);

Expand Down Expand Up @@ -910,7 +915,9 @@ public async ValueTask<ImmutableArray<ActiveStatementSpan>> GetAdjustedActiveSta
var oldProject = LastCommittedSolution.GetProject(projectId);
if (oldProject == null)
{
// project has been added (should have no active statements under normal circumstances)
// TODO: https://github.com/dotnet/roslyn/issues/1204
// project has been added - it may have active statements if the project was unloaded when debugging session started but the sources
// correspond to the PDB.
return null;
}

Expand All @@ -921,7 +928,7 @@ public async ValueTask<ImmutableArray<ActiveStatementSpan>> GetAdjustedActiveSta
return null;
}

documentId = await GetChangedDocumentContainingUnmappedActiveStatementAsync(activeStatementsMap, LastCommittedSolution, newProject, baseActiveStatement, cancellationToken).ConfigureAwait(false);
documentId = await GetChangedDocumentContainingUnmappedActiveStatementAsync(activeStatementsMap, LastCommittedSolution, oldProject, newProject, baseActiveStatement, cancellationToken).ConfigureAwait(false);
}
else
{
Expand All @@ -933,7 +940,14 @@ public async ValueTask<ImmutableArray<ActiveStatementSpan>> GetAdjustedActiveSta
async Task GetTaskAsync(ProjectId projectId)
{
var newProject = newSolution.GetRequiredProject(projectId);
var id = await GetChangedDocumentContainingUnmappedActiveStatementAsync(activeStatementsMap, LastCommittedSolution, newProject, baseActiveStatement, linkedTokenSource.Token).ConfigureAwait(false);
var oldProject = LastCommittedSolution.GetProject(projectId);

// TODO: https://github.com/dotnet/roslyn/issues/1204
// oldProject == null ==> project has been added - it may have active statements if the project was unloaded when debugging session started but the sources
// correspond to the PDB.
var id = (oldProject != null) ? await GetChangedDocumentContainingUnmappedActiveStatementAsync(
activeStatementsMap, LastCommittedSolution, oldProject, newProject, baseActiveStatement, linkedTokenSource.Token).ConfigureAwait(false) : null;

Interlocked.CompareExchange(ref documentId, id, null);
if (id != null)
{
Expand Down Expand Up @@ -963,11 +977,12 @@ async Task GetTaskAsync(ProjectId projectId)

// Enumerate all changed documents in the project whose module contains the active statement.
// For each such document enumerate all #line directives to find which maps code to the span that contains the active statement.
private static async ValueTask<DocumentId?> GetChangedDocumentContainingUnmappedActiveStatementAsync(ActiveStatementsMap baseActiveStatements, CommittedSolution oldSolution, Project newProject, ActiveStatement activeStatement, CancellationToken cancellationToken)
private static async ValueTask<DocumentId?> GetChangedDocumentContainingUnmappedActiveStatementAsync(ActiveStatementsMap baseActiveStatements, CommittedSolution oldSolution, Project oldProject, Project newProject, ActiveStatement activeStatement, CancellationToken cancellationToken)
{
Debug.Assert(oldProject.Id == newProject.Id);
var analyzer = newProject.LanguageServices.GetRequiredService<IEditAndContinueAnalyzer>();

await foreach (var documentId in EditSession.GetChangedDocumentsAsync(oldSolution, newProject, cancellationToken).ConfigureAwait(false))
await foreach (var documentId in EditSession.GetChangedDocumentsAsync(oldProject, newProject, cancellationToken).ConfigureAwait(false))
{
cancellationToken.ThrowIfCancellationRequested();

Expand Down
31 changes: 18 additions & 13 deletions src/Features/Core/Portable/EditAndContinue/EditSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -199,16 +199,21 @@ private static async Task PopulateChangedAndAddedDocumentsAsync(CommittedSolutio

var oldProject = oldSolution.GetProject(newProject.Id);

// When debugging session is started some projects might not have been loaded to the workspace yet.
// We capture the base solution. Edits in files that are in projects that haven't been loaded won't be applied
// and will result in source mismatch when the user steps into them.
//
// TODO (https://github.com/dotnet/roslyn/issues/1204):
// hook up the debugger reported error, check that the project has not been loaded and report a better error.
// Here, we assume these projects are not modified.
if (oldProject == null)
{
EditAndContinueWorkspaceService.Log.Write("EnC state of '{0}' [0x{1:X8}] queried: project not loaded", newProject.Id.DebugName, newProject.Id);

// TODO (https://github.com/dotnet/roslyn/issues/1204):
//
// When debugging session is started some projects might not have been loaded to the workspace yet (may be explicitly unloaded by the user).
// We capture the base solution. Edits in files that are in projects that haven't been loaded won't be applied
// and will result in source mismatch when the user steps into them.
//
// We can allow project to be added by including all its documents here.
// When we analyze these documents later on we'll check if they match the PDB.
// If so we can add them to the committed solution and detect further changes.
// It might be more efficient though to track added projects separately.

return;
}

Expand Down Expand Up @@ -293,9 +298,9 @@ private static async Task PopulateChangedAndAddedDocumentsAsync(CommittedSolutio
}
}

internal static async IAsyncEnumerable<DocumentId> GetChangedDocumentsAsync(CommittedSolution oldSolution, Project newProject, [EnumeratorCancellation] CancellationToken cancellationToken)
internal static async IAsyncEnumerable<DocumentId> GetChangedDocumentsAsync(Project oldProject, Project newProject, [EnumeratorCancellation] CancellationToken cancellationToken)
{
var oldProject = oldSolution.GetRequiredProject(newProject.Id);
Debug.Assert(oldProject.Id == newProject.Id);

if (!newProject.SupportsEditAndContinue() || oldProject.State == newProject.State)
{
Expand Down Expand Up @@ -739,10 +744,6 @@ public async ValueTask<SolutionUpdate> EmitSolutionUpdateAsync(Solution solution
continue;
}

// PopulateChangedAndAddedDocumentsAsync returns no changes if base project does not exist
var oldProject = oldSolution.GetProject(newProject.Id);
Contract.ThrowIfNull(oldProject);

// Ensure that all changed documents are in-sync. Once a document is in-sync it can't get out-of-sync.
// Therefore, results of further computations based on base snapshots of changed documents can't be invalidated by
// incoming events updating the content of out-of-sync documents.
Expand Down Expand Up @@ -819,6 +820,10 @@ public async ValueTask<SolutionUpdate> EmitSolutionUpdateAsync(Solution solution

EditAndContinueWorkspaceService.Log.Write("Emitting update of '{0}' [0x{1:X8}]", newProject.Id.DebugName, newProject.Id);

// PopulateChangedAndAddedDocumentsAsync returns no changes if base project does not exist
var oldProject = oldSolution.GetProject(newProject.Id);
Contract.ThrowIfNull(oldProject);

var oldCompilation = await oldProject.GetCompilationAsync(cancellationToken).ConfigureAwait(false);
var newCompilation = await newProject.GetCompilationAsync(cancellationToken).ConfigureAwait(false);
Contract.ThrowIfNull(oldCompilation);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ public IEnumerable<DocumentId> GetChangedStateIds(TextDocumentStates<TState> old
{
if (!oldStates.TryGetState(id, out var oldState))
{
// document was removed
// document was added
continue;
}

Expand Down