66using System . Collections . Generic ;
77using System . Diagnostics ;
88using System . Linq ;
9- using System . Threading ;
109using Microsoft . Build . BackEnd ;
1110using Microsoft . Build . BackEnd . Logging ;
1211using Microsoft . Build . BuildCheck . Infrastructure ;
@@ -59,7 +58,7 @@ public void ShutdownComponent()
5958 {
6059 _instance ? . Shutdown ( ) ;
6160 _instance = null ;
62- }
61+ }
6362
6463 internal sealed class BuildCheckManager : IBuildCheckManager , IBuildEngineDataRouter , IResultReporter
6564 {
@@ -94,7 +93,7 @@ public void SetDataSource(BuildCheckDataSource buildCheckDataSource)
9493 {
9594 _enabledDataSources [ ( int ) buildCheckDataSource ] = true ;
9695 RegisterBuiltInChecks ( buildCheckDataSource ) ;
97- }
96+ }
9897 stopwatch . Stop ( ) ;
9998 _tracingReporter . AddSetDataSourceStats ( stopwatch . Elapsed ) ;
10099 }
@@ -344,11 +343,11 @@ public void RemoveThrottledChecks(ICheckContext checkContext)
344343 private void RemoveCheck ( CheckFactoryContext checkToRemove )
345344 {
346345 _checkRegistry . Remove ( checkToRemove ) ;
347-
346+
348347 if ( checkToRemove . MaterializedCheck is not null )
349348 {
350349 _buildCheckCentralContext . DeregisterCheck ( checkToRemove . MaterializedCheck ) ;
351- _ruleTelemetryData . AddRange ( checkToRemove . MaterializedCheck . GetRuleTelemetryData ( ) ) ;
350+ _ruleTelemetryData . AddRange ( checkToRemove . MaterializedCheck . GetRuleTelemetryData ( ) ) ;
352351 checkToRemove . MaterializedCheck . Check . Dispose ( ) ;
353352 }
354353 }
@@ -372,6 +371,18 @@ public void ProcessEvaluationFinishedEventArgs(
372371 FileClassifier . Shared . RegisterKnownImmutableLocations ( getPropertyValue ) ;
373372 }
374373
374+ // run it here to avoid the missed imports that can be reported before the checks registration
375+ if ( _deferredProjectEvalIdToImportedProjects . TryGetValue ( checkContext . BuildEventContext . EvaluationId , out HashSet < string > ? importedProjects ) )
376+ {
377+ if ( importedProjects != null && TryGetProjectFullPath ( checkContext . BuildEventContext , out string projectPath ) )
378+ {
379+ foreach ( string importedProject in importedProjects )
380+ {
381+ _buildEventsProcessor . ProcessProjectImportedEventArgs ( checkContext , projectPath , importedProject ) ;
382+ }
383+ }
384+ }
385+
375386 _buildEventsProcessor
376387 . ProcessEvaluationFinishedEventArgs ( checkContext , evaluationFinishedEventArgs , propertiesLookup ) ;
377388 }
@@ -392,6 +403,16 @@ public void ProcessEnvironmentVariableReadEventArgs(ICheckContext checkContext,
392403 }
393404 }
394405
406+ public void ProcessProjectImportedEventArgs ( ICheckContext checkContext , ProjectImportedEventArgs projectImportedEventArgs )
407+ {
408+ if ( string . IsNullOrEmpty ( projectImportedEventArgs . ImportedProjectFile ) )
409+ {
410+ return ;
411+ }
412+
413+ PropagateImport ( checkContext . BuildEventContext . EvaluationId , projectImportedEventArgs . ProjectFile , projectImportedEventArgs . ImportedProjectFile ) ;
414+ }
415+
395416 public void ProcessTaskStartedEventArgs (
396417 ICheckContext checkContext ,
397418 TaskStartedEventArgs taskStartedEventArgs )
@@ -414,6 +435,7 @@ public void ProcessTaskParameterEventArgs(
414435 . ProcessTaskParameterEventArgs ( checkContext , taskParameterEventArgs ) ;
415436
416437 private readonly List < BuildCheckRuleTelemetryData > _ruleTelemetryData = [ ] ;
438+
417439 public BuildCheckTracingData CreateCheckTracingStats ( )
418440 {
419441 foreach ( CheckFactoryContext checkFactoryContext in _checkRegistry )
@@ -445,6 +467,8 @@ public void FinalizeProcessing(LoggingContext loggingContext)
445467 private readonly ConcurrentDictionary < int , string > _projectsByInstanceId = new ( ) ;
446468 private readonly ConcurrentDictionary < int , string > _projectsByEvaluationId = new ( ) ;
447469
470+ private readonly Dictionary < int , HashSet < string > > _deferredProjectEvalIdToImportedProjects = new ( ) ;
471+
448472 /// <summary>
449473 /// This method fetches the project full path from the context id.
450474 /// This is needed because the full path is needed for configuration and later for fetching configured checks
@@ -515,6 +539,10 @@ public void ProcessProjectEvaluationStarted(
515539 string projectFullPath )
516540 {
517541 _projectsByEvaluationId [ checkContext . BuildEventContext . EvaluationId ] = projectFullPath ;
542+ if ( ! _deferredProjectEvalIdToImportedProjects . ContainsKey ( checkContext . BuildEventContext . EvaluationId ) )
543+ {
544+ _deferredProjectEvalIdToImportedProjects . Add ( checkContext . BuildEventContext . EvaluationId , [ projectFullPath ] ) ;
545+ }
518546 }
519547
520548 /*
@@ -523,7 +551,6 @@ public void ProcessProjectEvaluationStarted(
523551 *
524552 */
525553
526-
527554 public void EndProjectEvaluation ( BuildEventContext buildEventContext )
528555 {
529556 }
@@ -548,6 +575,24 @@ public void StartProjectRequest(ICheckContext checkContext, string projectFullPa
548575 }
549576
550577 private readonly Dictionary < int , List < BuildEventArgs > > _deferredEvalDiagnostics = new ( ) ;
578+
579+ /// <summary>
580+ /// Propagates a newly imported project file to all projects that import the original project file.
581+ /// This method ensures that if Project A imports Project B, and Project B now imports Project C,
582+ /// then Project A will also show Project C as an import.
583+ /// </summary>
584+ /// <param name="evaluationId">The evaluation id is associated with the root project path.</param>
585+ /// <param name="originalProjectFile">The path of the project file that is importing a new project.</param>
586+ /// <param name="newImportedProjectFile">The path of the newly imported project file.</param>
587+ private void PropagateImport ( int evaluationId , string originalProjectFile , string newImportedProjectFile )
588+ {
589+ if ( _deferredProjectEvalIdToImportedProjects . TryGetValue ( evaluationId , out HashSet < string > ? importedProjects )
590+ && importedProjects . Contains ( originalProjectFile ) )
591+ {
592+ importedProjects . Add ( newImportedProjectFile ) ;
593+ }
594+ }
595+
551596 void IResultReporter . ReportResult ( BuildEventArgs eventArgs , ICheckContext checkContext )
552597 {
553598 // If we do not need to decide on promotability/demotability of warnings or we are ready to decide on those
0 commit comments