Skip to content

Commit cd02942

Browse files
committed
Calculate projects to deploy and add tests for adding projects.
Update HasChanges to reflect changes in project settings and references.
1 parent 858c3a8 commit cd02942

File tree

13 files changed

+622
-153
lines changed

13 files changed

+622
-153
lines changed

src/EditorFeatures/Test/EditAndContinue/EditAndContinueLanguageServiceTests.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,8 +210,9 @@ await localWorkspace.ChangeSolutionAsync(localWorkspace.CurrentSolution
210210
])
211211
],
212212
SyntaxError = syntaxError,
213-
ProjectsToRebuild = [project.Id],
214-
ProjectsToRestart = ImmutableDictionary<ProjectId, ImmutableArray<ProjectId>>.Empty.Add(project.Id, [])
213+
ProjectsToRebuild = [projectId],
214+
ProjectsToRestart = ImmutableDictionary<ProjectId, ImmutableArray<ProjectId>>.Empty.Add(projectId, []),
215+
ProjectsToRedeploy = [projectId],
215216
};
216217
};
217218

@@ -276,6 +277,7 @@ await localWorkspace.ChangeSolutionAsync(localWorkspace.CurrentSolution
276277
SyntaxError = null,
277278
ProjectsToRebuild = [],
278279
ProjectsToRestart = ImmutableDictionary<ProjectId, ImmutableArray<ProjectId>>.Empty,
280+
ProjectsToRedeploy = [],
279281
};
280282
};
281283

src/Features/Core/Portable/EditAndContinue/DebuggingSession.cs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -601,7 +601,8 @@ public async ValueTask<EmitSolutionUpdateResults> EmitSolutionUpdateAsync(
601601
Diagnostics = solutionUpdate.Diagnostics,
602602
SyntaxError = solutionUpdate.SyntaxError,
603603
ProjectsToRestart = solutionUpdate.ProjectsToRestart,
604-
ProjectsToRebuild = solutionUpdate.ProjectsToRebuild
604+
ProjectsToRebuild = solutionUpdate.ProjectsToRebuild,
605+
ProjectsToRedeploy = solutionUpdate.ProjectsToRedeploy,
605606
};
606607
}
607608

@@ -787,7 +788,8 @@ public async ValueTask<ImmutableArray<ImmutableArray<ActiveStatementSpan>>> GetB
787788
var oldProject = LastCommittedSolution.GetProject(projectId);
788789
if (oldProject == null)
789790
{
790-
// document is in a project that's been added to the solution
791+
// Document is in a project that's been added to the solution
792+
// No need to map the breakpoint from its original (base) location supplied by the debugger to a new one.
791793
continue;
792794
}
793795

@@ -807,7 +809,9 @@ public async ValueTask<ImmutableArray<ImmutableArray<ActiveStatementSpan>>> GetB
807809
var (oldDocument, _) = await LastCommittedSolution.GetDocumentAndStateAsync(newDocument, cancellationToken).ConfigureAwait(false);
808810
if (oldDocument == null)
809811
{
810-
// Document is out-of-sync, can't reason about its content with respect to the binaries loaded in the debuggee.
812+
// Document is either
813+
// 1) added -- no need to map the breakpoint from original location to a new one
814+
// 2) out-of-sync, in which case we can't reason about its content with respect to the binaries loaded in the debuggee.
811815
continue;
812816
}
813817

@@ -906,7 +910,7 @@ public async ValueTask<ImmutableArray<ActiveStatementSpan>> GetAdjustedActiveSta
906910
var oldProject = LastCommittedSolution.GetProject(newProject.Id);
907911
if (oldProject == null)
908912
{
909-
// TODO: https://github.com/dotnet/roslyn/issues/1204
913+
// TODO: https://github.com/dotnet/roslyn/issues/79423
910914
// Enumerate all documents of the new project.
911915
return [];
912916
}
@@ -939,7 +943,7 @@ public async ValueTask<ImmutableArray<ActiveStatementSpan>> GetAdjustedActiveSta
939943
var (oldUnmappedDocument, _) = await LastCommittedSolution.GetDocumentAndStateAsync(newUnmappedDocument, cancellationToken).ConfigureAwait(false);
940944
if (oldUnmappedDocument == null)
941945
{
942-
// document out-of-date
946+
// document added or out-of-date
943947
continue;
944948
}
945949

src/Features/Core/Portable/EditAndContinue/EditSession.cs

Lines changed: 99 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,11 @@ internal static async ValueTask<bool> HasDifferencesAsync(Project oldProject, Pr
343343
return false;
344344
}
345345

346+
if (HasProjectLevelDifferences(oldProject, newProject, differences) && differences == null)
347+
{
348+
return true;
349+
}
350+
346351
foreach (var documentId in newProject.State.DocumentStates.GetChangedStateIds(oldProject.State.DocumentStates, ignoreUnchangedContent: true))
347352
{
348353
var document = newProject.GetRequiredDocument(documentId);
@@ -361,7 +366,7 @@ internal static async ValueTask<bool> HasDifferencesAsync(Project oldProject, Pr
361366
return true;
362367
}
363368

364-
differences.Value.ChangedOrAddedDocuments.Add(document);
369+
differences.ChangedOrAddedDocuments.Add(document);
365370
}
366371

367372
foreach (var documentId in newProject.State.DocumentStates.GetAddedStateIds(oldProject.State.DocumentStates))
@@ -377,7 +382,7 @@ internal static async ValueTask<bool> HasDifferencesAsync(Project oldProject, Pr
377382
return true;
378383
}
379384

380-
differences.Value.ChangedOrAddedDocuments.Add(document);
385+
differences.ChangedOrAddedDocuments.Add(document);
381386
}
382387

383388
foreach (var documentId in newProject.State.DocumentStates.GetRemovedStateIds(oldProject.State.DocumentStates))
@@ -393,7 +398,7 @@ internal static async ValueTask<bool> HasDifferencesAsync(Project oldProject, Pr
393398
return true;
394399
}
395400

396-
differences.Value.DeletedDocuments.Add(document);
401+
differences.DeletedDocuments.Add(document);
397402
}
398403

399404
// The following will check for any changes in non-generated document content (editorconfig, additional docs).
@@ -436,10 +441,51 @@ internal static async ValueTask<bool> HasDifferencesAsync(Project oldProject, Pr
436441
return false;
437442
}
438443

439-
internal static async Task GetProjectDifferencesAsync(TraceLog log, Project oldProject, Project newProject, ProjectDifferences documentDifferences, ArrayBuilder<Diagnostic> diagnostics, CancellationToken cancellationToken)
444+
/// <summary>
445+
/// Return true if projects might have differences in state other than document content that migth affect EnC.
446+
/// The checks need to be fast. May return true even if the changes don't actually affect the behavior.
447+
/// </summary>
448+
internal static bool HasProjectLevelDifferences(Project oldProject, Project newProject, ProjectDifferences? differences)
449+
{
450+
if (oldProject.ParseOptions != newProject.ParseOptions ||
451+
oldProject.CompilationOptions != newProject.CompilationOptions ||
452+
oldProject.AssemblyName != newProject.AssemblyName)
453+
{
454+
if (differences != null)
455+
{
456+
differences.HasSettingChange = true;
457+
}
458+
else
459+
{
460+
return true;
461+
}
462+
}
463+
464+
if (!oldProject.MetadataReferences.SequenceEqual(newProject.MetadataReferences) ||
465+
!oldProject.ProjectReferences.SequenceEqual(newProject.ProjectReferences))
466+
{
467+
if (differences != null)
468+
{
469+
differences.HasReferenceChange = true;
470+
}
471+
else
472+
{
473+
return true;
474+
}
475+
}
476+
477+
return false;
478+
}
479+
480+
internal static async Task GetProjectDifferencesAsync(TraceLog log, Project? oldProject, Project newProject, ProjectDifferences documentDifferences, ArrayBuilder<Diagnostic> diagnostics, CancellationToken cancellationToken)
440481
{
441482
documentDifferences.Clear();
442483

484+
if (oldProject == null)
485+
{
486+
return;
487+
}
488+
443489
if (!await HasDifferencesAsync(oldProject, newProject, documentDifferences, cancellationToken).ConfigureAwait(false))
444490
{
445491
return;
@@ -697,6 +743,16 @@ private static bool HasReferenceRudeEdits(ImmutableDictionary<string, OneOrMany<
697743
return hasRudeEdit;
698744
}
699745

746+
private static bool HasAddedReference(Compilation oldCompilation, Compilation newCompilation)
747+
{
748+
using var pooledOldNames = SharedPools.StringIgnoreCaseHashSet.GetPooledObject();
749+
var oldNames = pooledOldNames.Object;
750+
Debug.Assert(oldNames.Comparer == AssemblyIdentityComparer.SimpleNameComparer);
751+
752+
oldNames.AddRange(oldCompilation.ReferencedAssemblyNames.Select(static r => r.Name));
753+
return newCompilation.ReferencedAssemblyNames.Any(static (newReference, oldNames) => !oldNames.Contains(newReference.Name), oldNames);
754+
}
755+
700756
internal static async ValueTask<ProjectChanges> GetProjectChangesAsync(
701757
ActiveStatementsMap baseActiveStatements,
702758
Compilation oldCompilation,
@@ -900,9 +956,11 @@ public async ValueTask<SolutionUpdate> EmitSolutionUpdateAsync(
900956
using var _1 = ArrayBuilder<ManagedHotReloadUpdate>.GetInstance(out var deltas);
901957
using var _2 = ArrayBuilder<(Guid ModuleId, ImmutableArray<(ManagedModuleMethodId Method, NonRemappableRegion Region)>)>.GetInstance(out var nonRemappableRegions);
902958
using var _3 = ArrayBuilder<ProjectBaseline>.GetInstance(out var newProjectBaselines);
903-
using var _4 = ArrayBuilder<(ProjectId id, Guid mvid)>.GetInstance(out var projectsToStale);
904-
using var _5 = ArrayBuilder<ProjectId>.GetInstance(out var projectsToUnstale);
959+
using var _4 = ArrayBuilder<ProjectId>.GetInstance(out var addedUnbuiltProjects);
960+
using var _5 = ArrayBuilder<ProjectId>.GetInstance(out var projectsToRedeploy);
905961
using var _6 = PooledDictionary<ProjectId, ArrayBuilder<Diagnostic>>.GetInstance(out var diagnosticBuilders);
962+
963+
// Project differences for currently analyzed project. Reused and cleared.
906964
using var projectDifferences = new ProjectDifferences();
907965

908966
// After all projects have been analyzed "true" value indicates changed document that is only included in stale projects.
@@ -945,39 +1003,16 @@ void UpdateChangedDocumentsStaleness(bool isStale)
9451003
}
9461004

9471005
var oldProject = oldSolution.GetProject(newProject.Id);
948-
if (oldProject == null)
949-
{
950-
Log.Write($"EnC state of {newProject.GetLogDisplay()} queried: project not loaded");
951-
952-
// TODO (https://github.com/dotnet/roslyn/issues/1204):
953-
//
954-
// When debugging session is started some projects might not have been loaded to the workspace yet (may be explicitly unloaded by the user).
955-
// We capture the base solution. Edits in files that are in projects that haven't been loaded won't be applied
956-
// and will result in source mismatch when the user steps into them.
957-
//
958-
// We can allow project to be added by including all its documents here.
959-
// When we analyze these documents later on we'll check if they match the PDB.
960-
// If so we can add them to the committed solution and detect further changes.
961-
// It might be more efficient though to track added projects separately.
962-
963-
continue;
964-
}
965-
966-
Debug.Assert(oldProject.SupportsEditAndContinue());
967-
968-
if (!oldProject.ProjectSettingsSupportEditAndContinue(Log))
969-
{
970-
// reason alrady reported
971-
continue;
972-
}
1006+
Debug.Assert(oldProject == null || oldProject.SupportsEditAndContinue());
9731007

9741008
projectDiagnostics = ArrayBuilder<Diagnostic>.GetInstance();
9751009

9761010
await GetProjectDifferencesAsync(Log, oldProject, newProject, projectDifferences, projectDiagnostics, cancellationToken).ConfigureAwait(false);
1011+
projectDifferences.Log(Log, newProject);
9771012

978-
if (projectDifferences.HasDocumentChanges)
1013+
if (projectDifferences.IsEmpty)
9791014
{
980-
Log.Write($"Found {projectDifferences.ChangedOrAddedDocuments.Count} potentially changed, {projectDifferences.DeletedDocuments.Count} deleted document(s) in project {newProject.GetLogDisplay()}");
1015+
continue;
9811016
}
9821017

9831018
var (mvid, mvidReadError) = await DebuggingSession.GetProjectModuleIdAsync(newProject, cancellationToken).ConfigureAwait(false);
@@ -989,8 +1024,9 @@ void UpdateChangedDocumentsStaleness(bool isStale)
9891024
if (mvid == staleModuleId || mvidReadError != null)
9901025
{
9911026
Log.Write($"EnC state of {newProject.GetLogDisplay()} queried: project is stale");
992-
UpdateChangedDocumentsStaleness(isStale: true);
9931027

1028+
// Track changed documents that are only included in stale or unbuilt projects:
1029+
UpdateChangedDocumentsStaleness(isStale: true);
9941030
continue;
9951031
}
9961032

@@ -1003,17 +1039,32 @@ void UpdateChangedDocumentsStaleness(bool isStale)
10031039
// The MVID is required for emit so we consider the error permanent and report it here.
10041040
// Bail before analyzing documents as the analysis needs to read the PDB which will likely fail if we can't even read the MVID.
10051041
projectDiagnostics.Add(mvidReadError);
1006-
projectSummaryToReport = ProjectAnalysisSummary.ValidChanges;
10071042
continue;
10081043
}
10091044

10101045
if (mvid == Guid.Empty)
10111046
{
1012-
Log.Write($"Changes not applied to {newProject.GetLogDisplay()}: project not built");
1047+
// If the project has been added to the solution, ask the project system to build it.
1048+
if (oldProject == null)
1049+
{
1050+
Log.Write($"Project build requested for {newProject.GetLogDisplay()}");
1051+
addedUnbuiltProjects.Add(newProject.Id);
1052+
}
1053+
else
1054+
{
1055+
Log.Write($"Changes not applied to {newProject.GetLogDisplay()}: project not built");
1056+
}
1057+
1058+
// Track changed documents that are only included in stale or unbuilt projects:
10131059
UpdateChangedDocumentsStaleness(isStale: true);
10141060
continue;
10151061
}
10161062

1063+
if (oldProject == null)
1064+
{
1065+
continue;
1066+
}
1067+
10171068
// Ensure that all changed documents are in-sync. Once a document is in-sync it can't get out-of-sync.
10181069
// Therefore, results of further computations based on base snapshots of changed documents can't be invalidated by
10191070
// incoming events updating the content of out-of-sync documents.
@@ -1079,8 +1130,7 @@ void UpdateChangedDocumentsStaleness(bool isStale)
10791130

10801131
// Unsupported changes in referenced assemblies will be reported below.
10811132
if (projectSummary is ProjectAnalysisSummary.NoChanges or ProjectAnalysisSummary.ValidInsignificantChanges &&
1082-
oldProject.MetadataReferences.SequenceEqual(newProject.MetadataReferences) &&
1083-
oldProject.ProjectReferences.SequenceEqual(newProject.ProjectReferences))
1133+
!projectDifferences.HasReferenceChange)
10841134
{
10851135
continue;
10861136
}
@@ -1140,6 +1190,14 @@ void UpdateChangedDocumentsStaleness(bool isStale)
11401190
continue;
11411191
}
11421192

1193+
// If the project references new dependencies, the host needs to invoke ReferenceCopyLocalPathsOutputGroup target on this project
1194+
// to deploy these dependencies to the projects output directory. The deployment shouldn't overwrite existing files.
1195+
// It should only happen if the project has no rude edits (especially not rude edits related to references) -- we bailed above if so.
1196+
if (HasAddedReference(oldCompilation, newCompilation))
1197+
{
1198+
projectsToRedeploy.Add(newProject.Id);
1199+
}
1200+
11431201
if (projectSummary is ProjectAnalysisSummary.NoChanges or ProjectAnalysisSummary.ValidInsignificantChanges)
11441202
{
11451203
continue;
@@ -1338,6 +1396,7 @@ async ValueTask LogDocumentChangesAsync(int? generation, CancellationToken cance
13381396
solution,
13391397
updates,
13401398
diagnostics,
1399+
addedUnbuiltProjects,
13411400
runningProjects,
13421401
out var projectsToRestart,
13431402
out var projectsToRebuild);
@@ -1352,7 +1411,8 @@ async ValueTask LogDocumentChangesAsync(int? generation, CancellationToken cance
13521411
diagnostics,
13531412
syntaxError: null,
13541413
projectsToRestart,
1355-
projectsToRebuild);
1414+
projectsToRebuild,
1415+
projectsToRedeploy.ToImmutable());
13561416
}
13571417
catch (Exception e) when (LogException(e) && FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken))
13581418
{

0 commit comments

Comments
 (0)