@@ -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,64 @@ 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
+ 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 )
440
494
{
441
495
documentDifferences . Clear ( ) ;
442
496
497
+ if ( oldProject == null )
498
+ {
499
+ return ;
500
+ }
501
+
443
502
if ( ! await HasDifferencesAsync ( oldProject , newProject , documentDifferences , cancellationToken ) . ConfigureAwait ( false ) )
444
503
{
445
504
return ;
@@ -697,6 +756,16 @@ private static bool HasReferenceRudeEdits(ImmutableDictionary<string, OneOrMany<
697
756
return hasRudeEdit ;
698
757
}
699
758
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
+
700
769
internal static async ValueTask < ProjectChanges > GetProjectChangesAsync (
701
770
ActiveStatementsMap baseActiveStatements ,
702
771
Compilation oldCompilation ,
@@ -900,9 +969,11 @@ public async ValueTask<SolutionUpdate> EmitSolutionUpdateAsync(
900
969
using var _1 = ArrayBuilder < ManagedHotReloadUpdate > . GetInstance ( out var deltas ) ;
901
970
using var _2 = ArrayBuilder < ( Guid ModuleId , ImmutableArray < ( ManagedModuleMethodId Method , NonRemappableRegion Region ) > ) > . GetInstance ( out var nonRemappableRegions ) ;
902
971
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 ) ;
905
974
using var _6 = PooledDictionary < ProjectId , ArrayBuilder < Diagnostic > > . GetInstance ( out var diagnosticBuilders ) ;
975
+
976
+ // Project differences for currently analyzed project. Reused and cleared.
906
977
using var projectDifferences = new ProjectDifferences ( ) ;
907
978
908
979
// 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)
945
1016
}
946
1017
947
1018
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 ( ) ) ;
975
1020
976
1021
await GetProjectDifferencesAsync ( Log , oldProject , newProject , projectDifferences , projectDiagnostics , cancellationToken ) . ConfigureAwait ( false ) ;
1022
+ projectDifferences . Log ( Log , newProject ) ;
977
1023
978
- if ( projectDifferences . HasDocumentChanges )
1024
+ if ( projectDifferences . IsEmpty )
979
1025
{
980
- Log . Write ( $ "Found { projectDifferences . ChangedOrAddedDocuments . Count } potentially changed, { projectDifferences . DeletedDocuments . Count } deleted document(s) in project { newProject . GetLogDisplay ( ) } " ) ;
1026
+ continue ;
981
1027
}
982
1028
983
1029
var ( mvid , mvidReadError ) = await DebuggingSession . GetProjectModuleIdAsync ( newProject , cancellationToken ) . ConfigureAwait ( false ) ;
@@ -989,8 +1035,9 @@ void UpdateChangedDocumentsStaleness(bool isStale)
989
1035
if ( mvid == staleModuleId || mvidReadError != null )
990
1036
{
991
1037
Log . Write ( $ "EnC state of { newProject . GetLogDisplay ( ) } queried: project is stale") ;
992
- UpdateChangedDocumentsStaleness ( isStale : true ) ;
993
1038
1039
+ // Track changed documents that are only included in stale or unbuilt projects:
1040
+ UpdateChangedDocumentsStaleness ( isStale : true ) ;
994
1041
continue ;
995
1042
}
996
1043
@@ -1003,17 +1050,32 @@ void UpdateChangedDocumentsStaleness(bool isStale)
1003
1050
// The MVID is required for emit so we consider the error permanent and report it here.
1004
1051
// 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
1052
projectDiagnostics . Add ( mvidReadError ) ;
1006
- projectSummaryToReport = ProjectAnalysisSummary . ValidChanges ;
1007
1053
continue ;
1008
1054
}
1009
1055
1010
1056
if ( mvid == Guid . Empty )
1011
1057
{
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:
1013
1070
UpdateChangedDocumentsStaleness ( isStale : true ) ;
1014
1071
continue ;
1015
1072
}
1016
1073
1074
+ if ( oldProject == null )
1075
+ {
1076
+ continue ;
1077
+ }
1078
+
1017
1079
// Ensure that all changed documents are in-sync. Once a document is in-sync it can't get out-of-sync.
1018
1080
// Therefore, results of further computations based on base snapshots of changed documents can't be invalidated by
1019
1081
// incoming events updating the content of out-of-sync documents.
@@ -1079,8 +1141,7 @@ void UpdateChangedDocumentsStaleness(bool isStale)
1079
1141
1080
1142
// Unsupported changes in referenced assemblies will be reported below.
1081
1143
if ( projectSummary is ProjectAnalysisSummary . NoChanges or ProjectAnalysisSummary . ValidInsignificantChanges &&
1082
- oldProject . MetadataReferences . SequenceEqual ( newProject . MetadataReferences ) &&
1083
- oldProject . ProjectReferences . SequenceEqual ( newProject . ProjectReferences ) )
1144
+ ! projectDifferences . HasReferenceChange )
1084
1145
{
1085
1146
continue ;
1086
1147
}
@@ -1140,6 +1201,14 @@ void UpdateChangedDocumentsStaleness(bool isStale)
1140
1201
continue ;
1141
1202
}
1142
1203
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
+
1143
1212
if ( projectSummary is ProjectAnalysisSummary . NoChanges or ProjectAnalysisSummary . ValidInsignificantChanges )
1144
1213
{
1145
1214
continue ;
@@ -1286,9 +1355,9 @@ async ValueTask LogDocumentChangesAsync(int? generation, CancellationToken cance
1286
1355
}
1287
1356
finally
1288
1357
{
1289
- if ( projectSummaryToReport . HasValue )
1358
+ if ( projectSummaryToReport . HasValue || ! projectDiagnostics . IsEmpty )
1290
1359
{
1291
- Telemetry . LogProjectAnalysisSummary ( projectSummaryToReport . Value , newProject . State . ProjectInfo . Attributes . TelemetryId , projectDiagnostics ) ;
1360
+ Telemetry . LogProjectAnalysisSummary ( projectSummaryToReport , newProject . State . ProjectInfo . Attributes . TelemetryId , projectDiagnostics ) ;
1292
1361
}
1293
1362
1294
1363
if ( ! projectDiagnostics . IsEmpty )
@@ -1338,6 +1407,7 @@ async ValueTask LogDocumentChangesAsync(int? generation, CancellationToken cance
1338
1407
solution ,
1339
1408
updates ,
1340
1409
diagnostics ,
1410
+ addedUnbuiltProjects ,
1341
1411
runningProjects ,
1342
1412
out var projectsToRestart ,
1343
1413
out var projectsToRebuild ) ;
@@ -1352,7 +1422,8 @@ async ValueTask LogDocumentChangesAsync(int? generation, CancellationToken cance
1352
1422
diagnostics ,
1353
1423
syntaxError : null ,
1354
1424
projectsToRestart ,
1355
- projectsToRebuild ) ;
1425
+ projectsToRebuild ,
1426
+ projectsToRedeploy . ToImmutable ( ) ) ;
1356
1427
}
1357
1428
catch ( Exception e ) when ( LogException ( e ) && FatalError . ReportAndPropagateUnlessCanceled ( e , cancellationToken ) )
1358
1429
{
0 commit comments