Skip to content

Commit 1a74e66

Browse files
authored
Projects to deploy (#79430)
1 parent 4ec123d commit 1a74e66

File tree

14 files changed

+645
-158
lines changed

14 files changed

+645
-158
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: 114 additions & 43 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,64 @@ 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+
Debug.Assert(oldProject.CompilationOptions != null);
451+
Debug.Assert(newProject.CompilationOptions != null);
452+
453+
if (oldProject.ParseOptions != newProject.ParseOptions ||
454+
HasDifferences(oldProject.CompilationOptions, newProject.CompilationOptions) ||
455+
oldProject.AssemblyName != newProject.AssemblyName)
456+
{
457+
if (differences != null)
458+
{
459+
differences.HasSettingChange = true;
460+
}
461+
else
462+
{
463+
return true;
464+
}
465+
}
466+
467+
if (!oldProject.MetadataReferences.SequenceEqual(newProject.MetadataReferences) ||
468+
!oldProject.ProjectReferences.SequenceEqual(newProject.ProjectReferences))
469+
{
470+
if (differences != null)
471+
{
472+
differences.HasReferenceChange = true;
473+
}
474+
else
475+
{
476+
return true;
477+
}
478+
}
479+
480+
return false;
481+
}
482+
483+
/// <summary>
484+
/// True if given compilation options differ in a way that might affect EnC.
485+
/// </summary>
486+
internal static bool HasDifferences(CompilationOptions oldOptions, CompilationOptions newOptions)
487+
=> !oldOptions
488+
.WithSyntaxTreeOptionsProvider(newOptions.SyntaxTreeOptionsProvider)
489+
.WithStrongNameProvider(newOptions.StrongNameProvider)
490+
.WithXmlReferenceResolver(newOptions.XmlReferenceResolver)
491+
.Equals(newOptions);
492+
493+
internal static async Task GetProjectDifferencesAsync(TraceLog log, Project? oldProject, Project newProject, ProjectDifferences documentDifferences, ArrayBuilder<Diagnostic> diagnostics, CancellationToken cancellationToken)
440494
{
441495
documentDifferences.Clear();
442496

497+
if (oldProject == null)
498+
{
499+
return;
500+
}
501+
443502
if (!await HasDifferencesAsync(oldProject, newProject, documentDifferences, cancellationToken).ConfigureAwait(false))
444503
{
445504
return;
@@ -697,6 +756,16 @@ private static bool HasReferenceRudeEdits(ImmutableDictionary<string, OneOrMany<
697756
return hasRudeEdit;
698757
}
699758

759+
private static bool HasAddedReference(Compilation oldCompilation, Compilation newCompilation)
760+
{
761+
using var pooledOldNames = SharedPools.StringIgnoreCaseHashSet.GetPooledObject();
762+
var oldNames = pooledOldNames.Object;
763+
Debug.Assert(oldNames.Comparer == AssemblyIdentityComparer.SimpleNameComparer);
764+
765+
oldNames.AddRange(oldCompilation.ReferencedAssemblyNames.Select(static r => r.Name));
766+
return newCompilation.ReferencedAssemblyNames.Any(static (newReference, oldNames) => !oldNames.Contains(newReference.Name), oldNames);
767+
}
768+
700769
internal static async ValueTask<ProjectChanges> GetProjectChangesAsync(
701770
ActiveStatementsMap baseActiveStatements,
702771
Compilation oldCompilation,
@@ -900,9 +969,11 @@ public async ValueTask<SolutionUpdate> EmitSolutionUpdateAsync(
900969
using var _1 = ArrayBuilder<ManagedHotReloadUpdate>.GetInstance(out var deltas);
901970
using var _2 = ArrayBuilder<(Guid ModuleId, ImmutableArray<(ManagedModuleMethodId Method, NonRemappableRegion Region)>)>.GetInstance(out var nonRemappableRegions);
902971
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);
972+
using var _4 = ArrayBuilder<ProjectId>.GetInstance(out var addedUnbuiltProjects);
973+
using var _5 = ArrayBuilder<ProjectId>.GetInstance(out var projectsToRedeploy);
905974
using var _6 = PooledDictionary<ProjectId, ArrayBuilder<Diagnostic>>.GetInstance(out var diagnosticBuilders);
975+
976+
// Project differences for currently analyzed project. Reused and cleared.
906977
using var projectDifferences = new ProjectDifferences();
907978

908979
// After all projects have been analyzed "true" value indicates changed document that is only included in stale projects.
@@ -945,39 +1016,14 @@ void UpdateChangedDocumentsStaleness(bool isStale)
9451016
}
9461017

9471018
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-
}
973-
974-
projectDiagnostics = ArrayBuilder<Diagnostic>.GetInstance();
1019+
Debug.Assert(oldProject == null || oldProject.SupportsEditAndContinue());
9751020

9761021
await GetProjectDifferencesAsync(Log, oldProject, newProject, projectDifferences, projectDiagnostics, cancellationToken).ConfigureAwait(false);
1022+
projectDifferences.Log(Log, newProject);
9771023

978-
if (projectDifferences.HasDocumentChanges)
1024+
if (projectDifferences.IsEmpty)
9791025
{
980-
Log.Write($"Found {projectDifferences.ChangedOrAddedDocuments.Count} potentially changed, {projectDifferences.DeletedDocuments.Count} deleted document(s) in project {newProject.GetLogDisplay()}");
1026+
continue;
9811027
}
9821028

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

1039+
// Track changed documents that are only included in stale or unbuilt projects:
1040+
UpdateChangedDocumentsStaleness(isStale: true);
9941041
continue;
9951042
}
9961043

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

10101056
if (mvid == Guid.Empty)
10111057
{
1012-
Log.Write($"Changes not applied to {newProject.GetLogDisplay()}: project not built");
1058+
// If the project has been added to the solution, ask the project system to build it.
1059+
if (oldProject == null)
1060+
{
1061+
Log.Write($"Project build requested for {newProject.GetLogDisplay()}");
1062+
addedUnbuiltProjects.Add(newProject.Id);
1063+
}
1064+
else
1065+
{
1066+
Log.Write($"Changes not applied to {newProject.GetLogDisplay()}: project not built");
1067+
}
1068+
1069+
// Track changed documents that are only included in stale or unbuilt projects:
10131070
UpdateChangedDocumentsStaleness(isStale: true);
10141071
continue;
10151072
}
10161073

1074+
if (oldProject == null)
1075+
{
1076+
continue;
1077+
}
1078+
10171079
// Ensure that all changed documents are in-sync. Once a document is in-sync it can't get out-of-sync.
10181080
// Therefore, results of further computations based on base snapshots of changed documents can't be invalidated by
10191081
// incoming events updating the content of out-of-sync documents.
@@ -1079,8 +1141,7 @@ void UpdateChangedDocumentsStaleness(bool isStale)
10791141

10801142
// Unsupported changes in referenced assemblies will be reported below.
10811143
if (projectSummary is ProjectAnalysisSummary.NoChanges or ProjectAnalysisSummary.ValidInsignificantChanges &&
1082-
oldProject.MetadataReferences.SequenceEqual(newProject.MetadataReferences) &&
1083-
oldProject.ProjectReferences.SequenceEqual(newProject.ProjectReferences))
1144+
!projectDifferences.HasReferenceChange)
10841145
{
10851146
continue;
10861147
}
@@ -1140,6 +1201,14 @@ void UpdateChangedDocumentsStaleness(bool isStale)
11401201
continue;
11411202
}
11421203

1204+
// If the project references new dependencies, the host needs to invoke ReferenceCopyLocalPathsOutputGroup target on this project
1205+
// to deploy these dependencies to the projects output directory. The deployment shouldn't overwrite existing files.
1206+
// It should only happen if the project has no rude edits (especially not rude edits related to references) -- we bailed above if so.
1207+
if (HasAddedReference(oldCompilation, newCompilation))
1208+
{
1209+
projectsToRedeploy.Add(newProject.Id);
1210+
}
1211+
11431212
if (projectSummary is ProjectAnalysisSummary.NoChanges or ProjectAnalysisSummary.ValidInsignificantChanges)
11441213
{
11451214
continue;
@@ -1286,9 +1355,9 @@ async ValueTask LogDocumentChangesAsync(int? generation, CancellationToken cance
12861355
}
12871356
finally
12881357
{
1289-
if (projectSummaryToReport.HasValue)
1358+
if (projectSummaryToReport.HasValue || !projectDiagnostics.IsEmpty)
12901359
{
1291-
Telemetry.LogProjectAnalysisSummary(projectSummaryToReport.Value, newProject.State.ProjectInfo.Attributes.TelemetryId, projectDiagnostics);
1360+
Telemetry.LogProjectAnalysisSummary(projectSummaryToReport, newProject.State.ProjectInfo.Attributes.TelemetryId, projectDiagnostics);
12921361
}
12931362

12941363
if (!projectDiagnostics.IsEmpty)
@@ -1338,6 +1407,7 @@ async ValueTask LogDocumentChangesAsync(int? generation, CancellationToken cance
13381407
solution,
13391408
updates,
13401409
diagnostics,
1410+
addedUnbuiltProjects,
13411411
runningProjects,
13421412
out var projectsToRestart,
13431413
out var projectsToRebuild);
@@ -1352,7 +1422,8 @@ async ValueTask LogDocumentChangesAsync(int? generation, CancellationToken cance
13521422
diagnostics,
13531423
syntaxError: null,
13541424
projectsToRestart,
1355-
projectsToRebuild);
1425+
projectsToRebuild,
1426+
projectsToRedeploy.ToImmutable());
13561427
}
13571428
catch (Exception e) when (LogException(e) && FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken))
13581429
{

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ public void LogAnalysisTime(TimeSpan span)
8787
public void LogSyntaxError()
8888
=> _hadSyntaxErrors = true;
8989

90-
public void LogProjectAnalysisSummary(ProjectAnalysisSummary summary, Guid projectTelemetryId, IEnumerable<Diagnostic> diagnostics)
90+
public void LogProjectAnalysisSummary(ProjectAnalysisSummary? summary, Guid projectTelemetryId, IEnumerable<Diagnostic> diagnostics)
9191
{
9292
lock (_guard)
9393
{
@@ -110,6 +110,10 @@ public void LogProjectAnalysisSummary(ProjectAnalysisSummary summary, Guid proje
110110

111111
switch (summary)
112112
{
113+
case null:
114+
// report diagnostics only
115+
break;
116+
113117
case ProjectAnalysisSummary.NoChanges:
114118
break;
115119

0 commit comments

Comments
 (0)