@@ -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 {
0 commit comments