@@ -343,6 +343,11 @@ internal static async ValueTask<bool> HasDifferencesAsync(Project oldProject, Pr
343
343
return false ;
344
344
}
345
345
346
+ if ( HasProjectLevelDifferences ( oldProject , newProject , differences ) && differences == null )
347
+ {
348
+ return true ;
349
+ }
350
+
346
351
foreach ( var documentId in newProject . State . DocumentStates . GetChangedStateIds ( oldProject . State . DocumentStates , ignoreUnchangedContent : true ) )
347
352
{
348
353
var document = newProject . GetRequiredDocument ( documentId ) ;
@@ -361,7 +366,7 @@ internal static async ValueTask<bool> HasDifferencesAsync(Project oldProject, Pr
361
366
return true ;
362
367
}
363
368
364
- differences . Value . ChangedOrAddedDocuments . Add ( document ) ;
369
+ differences . ChangedOrAddedDocuments . Add ( document ) ;
365
370
}
366
371
367
372
foreach ( var documentId in newProject . State . DocumentStates . GetAddedStateIds ( oldProject . State . DocumentStates ) )
@@ -377,7 +382,7 @@ internal static async ValueTask<bool> HasDifferencesAsync(Project oldProject, Pr
377
382
return true ;
378
383
}
379
384
380
- differences . Value . ChangedOrAddedDocuments . Add ( document ) ;
385
+ differences . ChangedOrAddedDocuments . Add ( document ) ;
381
386
}
382
387
383
388
foreach ( var documentId in newProject . State . DocumentStates . GetRemovedStateIds ( oldProject . State . DocumentStates ) )
@@ -393,7 +398,7 @@ internal static async ValueTask<bool> HasDifferencesAsync(Project oldProject, Pr
393
398
return true ;
394
399
}
395
400
396
- differences . Value . DeletedDocuments . Add ( document ) ;
401
+ differences . DeletedDocuments . Add ( document ) ;
397
402
}
398
403
399
404
// 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
436
441
return false ;
437
442
}
438
443
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 )
440
481
{
441
482
documentDifferences . Clear ( ) ;
442
483
484
+ if ( oldProject == null )
485
+ {
486
+ return ;
487
+ }
488
+
443
489
if ( ! await HasDifferencesAsync ( oldProject , newProject , documentDifferences , cancellationToken ) . ConfigureAwait ( false ) )
444
490
{
445
491
return ;
@@ -697,6 +743,16 @@ private static bool HasReferenceRudeEdits(ImmutableDictionary<string, OneOrMany<
697
743
return hasRudeEdit ;
698
744
}
699
745
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
+
700
756
internal static async ValueTask < ProjectChanges > GetProjectChangesAsync (
701
757
ActiveStatementsMap baseActiveStatements ,
702
758
Compilation oldCompilation ,
@@ -900,9 +956,11 @@ public async ValueTask<SolutionUpdate> EmitSolutionUpdateAsync(
900
956
using var _1 = ArrayBuilder < ManagedHotReloadUpdate > . GetInstance ( out var deltas ) ;
901
957
using var _2 = ArrayBuilder < ( Guid ModuleId , ImmutableArray < ( ManagedModuleMethodId Method , NonRemappableRegion Region ) > ) > . GetInstance ( out var nonRemappableRegions ) ;
902
958
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 ) ;
905
961
using var _6 = PooledDictionary < ProjectId , ArrayBuilder < Diagnostic > > . GetInstance ( out var diagnosticBuilders ) ;
962
+
963
+ // Project differences for currently analyzed project. Reused and cleared.
906
964
using var projectDifferences = new ProjectDifferences ( ) ;
907
965
908
966
// 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)
945
1003
}
946
1004
947
1005
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 ( ) ) ;
973
1007
974
1008
projectDiagnostics = ArrayBuilder < Diagnostic > . GetInstance ( ) ;
975
1009
976
1010
await GetProjectDifferencesAsync ( Log , oldProject , newProject , projectDifferences , projectDiagnostics , cancellationToken ) . ConfigureAwait ( false ) ;
1011
+ projectDifferences . Log ( Log , newProject ) ;
977
1012
978
- if ( projectDifferences . HasDocumentChanges )
1013
+ if ( projectDifferences . IsEmpty )
979
1014
{
980
- Log . Write ( $ "Found { projectDifferences . ChangedOrAddedDocuments . Count } potentially changed, { projectDifferences . DeletedDocuments . Count } deleted document(s) in project { newProject . GetLogDisplay ( ) } " ) ;
1015
+ continue ;
981
1016
}
982
1017
983
1018
var ( mvid , mvidReadError ) = await DebuggingSession . GetProjectModuleIdAsync ( newProject , cancellationToken ) . ConfigureAwait ( false ) ;
@@ -989,8 +1024,9 @@ void UpdateChangedDocumentsStaleness(bool isStale)
989
1024
if ( mvid == staleModuleId || mvidReadError != null )
990
1025
{
991
1026
Log . Write ( $ "EnC state of { newProject . GetLogDisplay ( ) } queried: project is stale") ;
992
- UpdateChangedDocumentsStaleness ( isStale : true ) ;
993
1027
1028
+ // Track changed documents that are only included in stale or unbuilt projects:
1029
+ UpdateChangedDocumentsStaleness ( isStale : true ) ;
994
1030
continue ;
995
1031
}
996
1032
@@ -1003,17 +1039,32 @@ void UpdateChangedDocumentsStaleness(bool isStale)
1003
1039
// The MVID is required for emit so we consider the error permanent and report it here.
1004
1040
// Bail before analyzing documents as the analysis needs to read the PDB which will likely fail if we can't even read the MVID.
1005
1041
projectDiagnostics . Add ( mvidReadError ) ;
1006
- projectSummaryToReport = ProjectAnalysisSummary . ValidChanges ;
1007
1042
continue ;
1008
1043
}
1009
1044
1010
1045
if ( mvid == Guid . Empty )
1011
1046
{
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:
1013
1059
UpdateChangedDocumentsStaleness ( isStale : true ) ;
1014
1060
continue ;
1015
1061
}
1016
1062
1063
+ if ( oldProject == null )
1064
+ {
1065
+ continue ;
1066
+ }
1067
+
1017
1068
// Ensure that all changed documents are in-sync. Once a document is in-sync it can't get out-of-sync.
1018
1069
// Therefore, results of further computations based on base snapshots of changed documents can't be invalidated by
1019
1070
// incoming events updating the content of out-of-sync documents.
@@ -1079,8 +1130,7 @@ void UpdateChangedDocumentsStaleness(bool isStale)
1079
1130
1080
1131
// Unsupported changes in referenced assemblies will be reported below.
1081
1132
if ( projectSummary is ProjectAnalysisSummary . NoChanges or ProjectAnalysisSummary . ValidInsignificantChanges &&
1082
- oldProject . MetadataReferences . SequenceEqual ( newProject . MetadataReferences ) &&
1083
- oldProject . ProjectReferences . SequenceEqual ( newProject . ProjectReferences ) )
1133
+ ! projectDifferences . HasReferenceChange )
1084
1134
{
1085
1135
continue ;
1086
1136
}
@@ -1140,6 +1190,14 @@ void UpdateChangedDocumentsStaleness(bool isStale)
1140
1190
continue ;
1141
1191
}
1142
1192
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
+
1143
1201
if ( projectSummary is ProjectAnalysisSummary . NoChanges or ProjectAnalysisSummary . ValidInsignificantChanges )
1144
1202
{
1145
1203
continue ;
@@ -1338,6 +1396,7 @@ async ValueTask LogDocumentChangesAsync(int? generation, CancellationToken cance
1338
1396
solution ,
1339
1397
updates ,
1340
1398
diagnostics ,
1399
+ addedUnbuiltProjects ,
1341
1400
runningProjects ,
1342
1401
out var projectsToRestart ,
1343
1402
out var projectsToRebuild ) ;
@@ -1352,7 +1411,8 @@ async ValueTask LogDocumentChangesAsync(int? generation, CancellationToken cance
1352
1411
diagnostics ,
1353
1412
syntaxError : null ,
1354
1413
projectsToRestart ,
1355
- projectsToRebuild ) ;
1414
+ projectsToRebuild ,
1415
+ projectsToRedeploy . ToImmutable ( ) ) ;
1356
1416
}
1357
1417
catch ( Exception e ) when ( LogException ( e ) && FatalError . ReportAndPropagateUnlessCanceled ( e , cancellationToken ) )
1358
1418
{
0 commit comments