File: BuildCheck\Infrastructure\BuildCheckManagerProvider.cs
Web Access
Project: ..\..\..\src\Build\Microsoft.Build.csproj (Microsoft.Build)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Microsoft.Build.BackEnd;
using Microsoft.Build.BackEnd.Logging;
using Microsoft.Build.BuildCheck.Infrastructure;
using Microsoft.Build.Construction;
using Microsoft.Build.Experimental.BuildCheck.Acquisition;
using Microsoft.Build.Experimental.BuildCheck.Checks;
using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
 
namespace Microsoft.Build.Experimental.BuildCheck.Infrastructure;
 
internal delegate Check CheckFactory();
internal delegate CheckWrapper CheckWrapperFactory(ConfigurationContext configurationContext);
 
/// <summary>
/// The central manager for the BuildCheck - this is the integration point with MSBuild infrastructure.
/// </summary>
internal sealed class BuildCheckManagerProvider : IBuildCheckManagerProvider
{
    private IBuildCheckManager? _instance;
 
    public IBuildCheckManager Instance => _instance ?? new NullBuildCheckManager();
 
    public IBuildEngineDataRouter BuildEngineDataRouter => (IBuildEngineDataRouter)Instance;
 
    internal static IBuildComponent CreateComponent(BuildComponentType type)
    {
        ErrorUtilities.VerifyThrow(type == BuildComponentType.BuildCheckManagerProvider, "Cannot create components of type {0}", type);
        return new BuildCheckManagerProvider();
    }
 
    public void InitializeComponent(IBuildComponentHost host)
    {
        ErrorUtilities.VerifyThrow(host != null, "BuildComponentHost was null");
 
        if (_instance == null)
        {
            if (host!.BuildParameters.IsBuildCheckEnabled)
            {
                _instance = new BuildCheckManager();
            }
            else
            {
                _instance = new NullBuildCheckManager();
            }
        }
    }
 
    public void ShutdownComponent()
    {
        _instance?.Shutdown();
        _instance = null;
    }
 
    internal sealed class BuildCheckManager : IBuildCheckManager, IBuildEngineDataRouter, IResultReporter
    {
        private readonly TracingReporter _tracingReporter = new TracingReporter();
        private readonly IConfigurationProvider _configurationProvider = new ConfigurationProvider();
        private readonly BuildCheckCentralContext _buildCheckCentralContext;
        private readonly List<CheckFactoryContext> _checkRegistry;
        private readonly bool[] _enabledDataSources = new bool[(int)BuildCheckDataSource.ValuesCount];
        private readonly BuildEventsProcessor _buildEventsProcessor;
        private readonly IBuildCheckAcquisitionModule _acquisitionModule;
 
        internal BuildCheckManager()
        {
            _checkRegistry = new List<CheckFactoryContext>();
            _acquisitionModule = new BuildCheckAcquisitionModule();
            _buildCheckCentralContext = new(_configurationProvider, RemoveChecksAfterExecutedActions);
            _buildEventsProcessor = new(_buildCheckCentralContext);
        }
 
        private bool IsInProcNode => _enabledDataSources[(int)BuildCheckDataSource.EventArgs] &&
                                     _enabledDataSources[(int)BuildCheckDataSource.BuildExecution];
 
        /// <summary>
        /// Notifies the manager that the data source will be used -
        ///   so it should register the built-in checks for the source if it hasn't been done yet.
        /// </summary>
        /// <param name="buildCheckDataSource"></param>
        public void SetDataSource(BuildCheckDataSource buildCheckDataSource)
        {
            Stopwatch stopwatch = Stopwatch.StartNew();
            if (!_enabledDataSources[(int)buildCheckDataSource])
            {
                _enabledDataSources[(int)buildCheckDataSource] = true;
                RegisterBuiltInChecks(buildCheckDataSource);
            }
            stopwatch.Stop();
            _tracingReporter.AddSetDataSourceStats(stopwatch.Elapsed);
        }
 
        public void ProcessCheckAcquisition(
            CheckAcquisitionData acquisitionData,
            ICheckContext checkContext)
        {
            Stopwatch stopwatch = Stopwatch.StartNew();
            if (IsInProcNode)
            {
                var checksFactories = _acquisitionModule.CreateCheckFactories(acquisitionData, checkContext);
                if (checksFactories.Count != 0)
                {
                    RegisterCustomCheck(acquisitionData.ProjectPath, BuildCheckDataSource.EventArgs, checksFactories, checkContext);
                }
                else
                {
                    checkContext.DispatchAsComment(MessageImportance.Normal, "CustomCheckFailedAcquisition", acquisitionData.AssemblyPath);
                }
            }
            else
            {
                BuildCheckAcquisitionEventArgs eventArgs = acquisitionData.ToBuildEventArgs();
                eventArgs.BuildEventContext = checkContext.BuildEventContext!;
 
                checkContext.DispatchBuildEvent(eventArgs);
            }
 
            stopwatch.Stop();
            _tracingReporter.AddAcquisitionStats(stopwatch.Elapsed);
        }
 
        private static T Construct<T>() where T : new() => new();
 
        /// <summary>
        /// The builtin check factory definition
        /// </summary>
        /// <param name="RuleIds">The rule ids that the check is able to emit.</param>
        /// <param name="DefaultEnablement">Is it enabled by default?</param>
        /// <param name="Factory">Factory method to create the check.</param>
        internal readonly record struct BuiltInCheckFactory(
            string[] RuleIds,
            bool DefaultEnablement,
            CheckFactory Factory);
 
        private static readonly BuiltInCheckFactory[][] s_builtInFactoriesPerDataSource =
        [
 
            // BuildCheckDataSource.EventArgs
            [
                new BuiltInCheckFactory([SharedOutputPathCheck.SupportedRule.Id], SharedOutputPathCheck.SupportedRule.DefaultConfiguration.IsEnabled ?? false, Construct<SharedOutputPathCheck>),
                new BuiltInCheckFactory([PreferProjectReferenceCheck.SupportedRule.Id], PreferProjectReferenceCheck.SupportedRule.DefaultConfiguration.IsEnabled ?? false, Construct<PreferProjectReferenceCheck>),
                new BuiltInCheckFactory([DoubleWritesCheck.SupportedRule.Id], DoubleWritesCheck.SupportedRule.DefaultConfiguration.IsEnabled ?? false, Construct<DoubleWritesCheck>),
                new BuiltInCheckFactory([NoEnvironmentVariablePropertyCheck.SupportedRule.Id], NoEnvironmentVariablePropertyCheck.SupportedRule.DefaultConfiguration.IsEnabled ?? false, Construct<NoEnvironmentVariablePropertyCheck>)
            ],
 
            // BuildCheckDataSource.Execution
            [
                new BuiltInCheckFactory(PropertiesUsageCheck.SupportedRulesList.Select(r => r.Id).ToArray(),
                    PropertiesUsageCheck.SupportedRulesList.Any(r => r.DefaultConfiguration.IsEnabled ?? false),
                    Construct<PropertiesUsageCheck>)
            ]
        ];
 
        /// <summary>
        /// For tests only. TODO: Remove when check acquisition is done.
        /// </summary>
        internal static BuiltInCheckFactory[][]? s_testFactoriesPerDataSource;
 
        private void RegisterBuiltInChecks(BuildCheckDataSource buildCheckDataSource)
        {
            _checkRegistry.AddRange(
                s_builtInFactoriesPerDataSource[(int)buildCheckDataSource]
                    .Select(v => new CheckFactoryContext(v.Factory, v.RuleIds, v.DefaultEnablement)));
 
            if (s_testFactoriesPerDataSource is not null)
            {
                _checkRegistry.AddRange(
                    s_testFactoriesPerDataSource[(int)buildCheckDataSource]
                        .Select(v => new CheckFactoryContext(v.Factory, v.RuleIds, v.DefaultEnablement)));
            }
        }
 
        /// <summary>
        /// To be used by acquisition module
        /// Registers the custom check, the construction of check is needed during registration.
        /// </summary>
        /// <param name="projectPath">The project path is used for the correct .editorconfig resolution.</param>
        /// <param name="buildCheckDataSource">Represents different data sources used in build check operations.</param>
        /// <param name="factories">A collection of build check factories for rules instantiation.</param>
        /// <param name="checkContext">The logging context of the build event.</param>
        internal void RegisterCustomCheck(
            string projectPath,
            BuildCheckDataSource buildCheckDataSource,
            IEnumerable<CheckFactory> factories,
            ICheckContext checkContext)
        {
            if (_enabledDataSources[(int)buildCheckDataSource])
            {
                List<CheckFactoryContext> invalidChecksToRemove = new();
                foreach (var factory in factories)
                {
                    var instance = factory();
                    if (instance != null && instance.SupportedRules.Any())
                    {
                        var checkFactoryContext = new CheckFactoryContext(
                            factory,
                            instance.SupportedRules.Select(r => r.Id).ToArray(),
                            instance.SupportedRules.Any(r => r.DefaultConfiguration.IsEnabled == true));
 
                        if (checkFactoryContext != null)
                        {
                            _checkRegistry.Add(checkFactoryContext);
                            try
                            {
                                SetupSingleCheck(checkFactoryContext, projectPath);
                                checkContext.DispatchAsComment(MessageImportance.Normal, "CustomCheckSuccessfulAcquisition", instance.FriendlyName);
                            }
                            catch (BuildCheckConfigurationException e)
                            {
                                checkContext.DispatchAsWarningFromText(
                                    null,
                                    null,
                                    null,
                                    new BuildEventFileInfo(projectPath),
                                    e.Message);
                                invalidChecksToRemove.Add(checkFactoryContext);
                            }
                        }
                    }
                    RemoveInvalidChecks(invalidChecksToRemove, checkContext);
                }
            }
        }
 
        private void SetupSingleCheck(CheckFactoryContext checkFactoryContext, string projectFullPath)
        {
            // For custom checks - it should run only on projects where referenced
            // (otherwise error out - https://github.com/orgs/dotnet/projects/373/views/1?pane=issue&itemId=57849480)
            // on others it should work similarly as disabling them.
            // Disabled check should not only post-filter results - it shouldn't even see the data
            CheckWrapper wrapper;
            CheckConfigurationEffective[] configurations;
            if (checkFactoryContext.MaterializedCheck == null)
            {
                CheckConfiguration[] userEditorConfigs =
                    _configurationProvider.GetUserConfigurations(projectFullPath, checkFactoryContext.RuleIds);
 
                if (userEditorConfigs.All(c => !(c.IsEnabled ?? checkFactoryContext.IsEnabledByDefault)))
                {
                    // the check was not yet instantiated nor mounted - so nothing to do here now.
                    return;
                }
 
                CustomConfigurationData[] customConfigData =
                    _configurationProvider.GetCustomConfigurations(projectFullPath, checkFactoryContext.RuleIds);
 
                Check uninitializedCheck = checkFactoryContext.Factory();
                configurations = _configurationProvider.GetMergedConfigurations(userEditorConfigs, uninitializedCheck);
 
                ConfigurationContext configurationContext = ConfigurationContext.FromDataEnumeration(customConfigData, configurations);
 
                wrapper = checkFactoryContext.Initialize(uninitializedCheck, this, configurationContext);
                checkFactoryContext.MaterializedCheck = wrapper;
                Check check = wrapper.Check;
 
                // This is to facilitate possible perf improvement for custom checks - as we might want to
                //  avoid loading the assembly and type just to check if it's supported.
                // If we expose a way to declare the enablement status and rule ids during registration (e.g. via
                //  optional arguments of the intrinsic property function) - we can then avoid loading it.
                // But once loaded - we should verify that the declared enablement status and rule ids match the actual ones.
                if (
                    check.SupportedRules.Count != checkFactoryContext.RuleIds.Length
                    ||
                    !check.SupportedRules.Select(r => r.Id)
                        .SequenceEqual(checkFactoryContext.RuleIds, StringComparer.CurrentCultureIgnoreCase)
                )
                {
                    throw new BuildCheckConfigurationException(
                        $"The check '{check.FriendlyName}' exposes rules '{check.SupportedRules.Select(r => r.Id).ToCsvString()}', but different rules were declared during registration: '{checkFactoryContext.RuleIds.ToCsvString()}'");
                }
 
                // technically all checks rules could be disabled, but that would mean
                // that the provided 'IsEnabledByDefault' value wasn't correct - the only
                // price to be paid in that case is slight performance cost.
 
                // Create the wrapper and register to central context
                wrapper.StartNewProject(projectFullPath, configurations, userEditorConfigs);
                var wrappedContext = new CheckRegistrationContext(wrapper, _buildCheckCentralContext);
                try
                {
                    check.RegisterActions(wrappedContext);
                }
                catch (Exception e)
                {
                    string message = $"The check '{check.FriendlyName}' failed to register actions with the following message: '{e.Message}'";
                    throw new BuildCheckConfigurationException(message, e);
                }
            }
            else
            {
                wrapper = checkFactoryContext.MaterializedCheck;
 
                CheckConfiguration[] userEditorConfigs =
                    _configurationProvider.GetUserConfigurations(projectFullPath, checkFactoryContext.RuleIds);
                configurations = _configurationProvider.GetMergedConfigurations(userEditorConfigs, wrapper.Check);
 
                _configurationProvider.CheckCustomConfigurationDataValidity(projectFullPath,
                    checkFactoryContext.RuleIds[0]);
 
                // Update the wrapper
                wrapper.StartNewProject(projectFullPath, configurations, userEditorConfigs);
            }
        }
 
        private void SetupChecksForNewProject(string projectFullPath, ICheckContext checkContext)
        {
            // Only add checks here
            // On an execution node - we might remove and dispose the checks once project is done
 
            // If it's already constructed - just control the custom settings do not differ
            Stopwatch stopwatch = Stopwatch.StartNew();
            List<CheckFactoryContext> invalidChecksToRemove = new();
            foreach (CheckFactoryContext checkFactoryContext in _checkRegistry)
            {
                try
                {
                    SetupSingleCheck(checkFactoryContext, projectFullPath);
                }
                catch (BuildCheckConfigurationException e)
                {
                    checkContext.DispatchAsWarningFromText(
                        null,
                        null,
                        null,
                        new BuildEventFileInfo(projectFullPath),
                        e.Message);
                    invalidChecksToRemove.Add(checkFactoryContext);
                }
            }
 
            RemoveInvalidChecks(invalidChecksToRemove, checkContext);
 
            stopwatch.Stop();
            _tracingReporter.AddNewProjectStats(stopwatch.Elapsed);
        }
 
        private void RemoveInvalidChecks(List<CheckFactoryContext> checksToRemove, ICheckContext checkContext)
        {
            foreach (var checkToRemove in checksToRemove)
            {
                checkContext.DispatchAsCommentFromText(MessageImportance.High, $"Dismounting check '{checkToRemove.FriendlyName}'");
                RemoveCheck(checkToRemove);
            }
        }
 
        public void RemoveChecksAfterExecutedActions(List<CheckWrapper>? checksToRemove, ICheckContext checkContext)
        {
            if (checksToRemove is not null)
            {
                foreach (CheckWrapper check in checksToRemove)
                {
                    var checkFactory = _checkRegistry.Find(c => c.MaterializedCheck == check);
                    if (checkFactory is not null)
                    {
                        checkContext.DispatchAsCommentFromText(MessageImportance.High, $"Dismounting check '{check.Check.FriendlyName}'. The check has thrown an unhandled exception while executing registered actions.");
                        RemoveCheck(checkFactory);
                    }
                }
            }
 
            foreach (var throttledCheck in _checkRegistry.FindAll(c => c.MaterializedCheck?.IsThrottled ?? false))
            {
                checkContext.DispatchAsCommentFromText(MessageImportance.Normal, $"Dismounting check '{throttledCheck.FriendlyName}'. The check has exceeded the maximum number of results allowed. Any additional results will not be displayed.");
                RemoveCheck(throttledCheck);
            }
        }
 
        private void RemoveCheck(CheckFactoryContext checkToRemove)
        {
            _checkRegistry.Remove(checkToRemove);
 
            if (checkToRemove.MaterializedCheck is not null)
            {
                _buildCheckCentralContext.DeregisterCheck(checkToRemove.MaterializedCheck);
                _ruleTelemetryData.AddRange(checkToRemove.MaterializedCheck.GetRuleTelemetryData());
                checkToRemove.MaterializedCheck.Check.Dispose();
            }
        }
 
        public void ProcessEvaluationFinishedEventArgs(
            ICheckContext checkContext,
            ProjectEvaluationFinishedEventArgs evaluationFinishedEventArgs)
        {
            Dictionary<string, string>? propertiesLookup = null;
 
            // The FileClassifier is normally initialized by executing build requests.
            // However, if we are running in a main node that has no execution nodes - we need to initialize it here (from events).
            if (!IsInProcNode)
            {
                propertiesLookup =
                    BuildEventsProcessor.ExtractPropertiesLookup(evaluationFinishedEventArgs);
                Func<string, string?> getPropertyValue = p =>
                    propertiesLookup.TryGetValue(p, out string? value) ? value : null;
 
                FileClassifier.Shared.RegisterFrameworkLocations(getPropertyValue);
                FileClassifier.Shared.RegisterKnownImmutableLocations(getPropertyValue);
            }
 
            // run it here to avoid the missed imports that can be reported before the checks registration
            if (_deferredProjectEvalIdToImportedProjects.TryGetValue(checkContext.BuildEventContext.EvaluationId, out HashSet<string>? importedProjects))
            {
                if (importedProjects != null && TryGetProjectFullPath(checkContext.BuildEventContext, out string projectPath))
                {
                    lock (importedProjects)
                    {
                        foreach (string importedProject in importedProjects)
                        {
                            _buildEventsProcessor.ProcessProjectImportedEventArgs(checkContext, projectPath, importedProject);
                        }
                    }
                }
            }
 
            _buildEventsProcessor
                .ProcessEvaluationFinishedEventArgs(checkContext, evaluationFinishedEventArgs, propertiesLookup);
        }
 
        public void ProcessEnvironmentVariableReadEventArgs(ICheckContext checkContext, EnvironmentVariableReadEventArgs projectEvaluationEventArgs)
        {
            if (projectEvaluationEventArgs is EnvironmentVariableReadEventArgs evr)
            {
                if (TryGetProjectFullPath(checkContext.BuildEventContext, out string projectPath))
                {
                    _buildEventsProcessor.ProcessEnvironmentVariableReadEventArgs(
                        checkContext,
                        projectPath,
                        evr.EnvironmentVariableName,
                        evr.Message ?? string.Empty,
                        ElementLocation.Create(evr.File, evr.LineNumber, evr.ColumnNumber));
                }
            }
        }
 
        public void ProcessProjectImportedEventArgs(ICheckContext checkContext, ProjectImportedEventArgs projectImportedEventArgs)
        {
            if (string.IsNullOrEmpty(projectImportedEventArgs.ImportedProjectFile))
            {
                return;
            }
 
            PropagateImport(checkContext.BuildEventContext.EvaluationId, projectImportedEventArgs.ProjectFile, projectImportedEventArgs.ImportedProjectFile);
        }
 
        public void ProcessTaskStartedEventArgs(
            ICheckContext checkContext,
            TaskStartedEventArgs taskStartedEventArgs)
            => _buildEventsProcessor
                .ProcessTaskStartedEventArgs(checkContext, taskStartedEventArgs);
 
        public void ProcessBuildFinished(ICheckContext checkContext)
            => _buildEventsProcessor.ProcessBuildDone(checkContext);
 
        public void ProcessTaskFinishedEventArgs(
            ICheckContext checkContext,
            TaskFinishedEventArgs taskFinishedEventArgs)
            => _buildEventsProcessor
                .ProcessTaskFinishedEventArgs(checkContext, taskFinishedEventArgs);
 
        public void ProcessTaskParameterEventArgs(
            ICheckContext checkContext,
            TaskParameterEventArgs taskParameterEventArgs)
            => _buildEventsProcessor
                .ProcessTaskParameterEventArgs(checkContext, taskParameterEventArgs);
 
        private readonly List<BuildCheckRuleTelemetryData> _ruleTelemetryData = [];
 
        public BuildCheckTracingData CreateCheckTracingStats()
        {
            foreach (CheckFactoryContext checkFactoryContext in _checkRegistry)
            {
                if (checkFactoryContext.MaterializedCheck != null)
                {
                    _ruleTelemetryData.AddRange(checkFactoryContext.MaterializedCheck.GetRuleTelemetryData());
                }
            }
 
            return new BuildCheckTracingData(_ruleTelemetryData, _tracingReporter.GetInfrastructureTracingStats());
        }
 
        public void FinalizeProcessing(LoggingContext loggingContext)
        {
            if (IsInProcNode)
            {
                // We do not want to send tracing stats from in-proc node
                return;
            }
 
            var checkEventStats = CreateCheckTracingStats();
 
            BuildCheckTracingEventArgs checkEventArg =
                new(checkEventStats) { BuildEventContext = loggingContext.BuildEventContext };
            loggingContext.LogBuildEvent(checkEventArg);
        }
 
        private readonly ConcurrentDictionary<int, string> _projectsByInstanceId = new();
        private readonly ConcurrentDictionary<int, string> _projectsByEvaluationId = new();
 
        private readonly ConcurrentDictionary<int, HashSet<string>> _deferredProjectEvalIdToImportedProjects = new();
 
        /// <summary>
        /// This method fetches the project full path from the context id.
        /// This is needed because the full path is needed for configuration and later for fetching configured checks
        ///  (future version might optimize by using the ProjectContextId directly for fetching the checks).
        /// </summary>
        /// <param name="buildEventContext"></param>
        /// <param name="projectFullPath"></param>
        /// <returns></returns>
        private bool TryGetProjectFullPath(BuildEventContext buildEventContext, out string projectFullPath)
        {
            if (buildEventContext.EvaluationId >= 0)
            {
                if (_projectsByEvaluationId.TryGetValue(buildEventContext.EvaluationId, out string? val))
                {
                    projectFullPath = val;
                    return true;
                }
            }
            else if (buildEventContext.ProjectInstanceId >= 0)
            {
                if (_projectsByInstanceId.TryGetValue(buildEventContext.ProjectInstanceId, out string? val))
                {
                    projectFullPath = val;
                    return true;
                }
            }
            else if (_projectsByInstanceId.Count == 1)
            {
                projectFullPath = _projectsByInstanceId.FirstOrDefault().Value;
                // This is for a rare possibility of a race where other thread removed the item (between the if check and fetch here).
                // We currently do not support multiple projects in parallel in a single node anyway.
                if (!string.IsNullOrEmpty(projectFullPath))
                {
                    return true;
                }
            }
            else if (_projectsByEvaluationId.Count == 1)
            {
                projectFullPath = _projectsByEvaluationId.FirstOrDefault().Value;
                if (!string.IsNullOrEmpty(projectFullPath))
                {
                    return true;
                }
            }
 
            projectFullPath = string.Empty;
            return false;
        }
 
        public void ProjectFirstEncountered(
            BuildCheckDataSource buildCheckDataSource,
            ICheckContext checkContext,
            string projectFullPath)
        {
            if (buildCheckDataSource == BuildCheckDataSource.EventArgs && IsInProcNode)
            {
                // Skipping this event - as it was already handled by the in-proc node.
                // This is because in-proc node has the BuildEventArgs source and check source
                //  both in a single manager. The project started is first encountered by the execution before the EventArg is sent
                return;
            }
 
            SetupChecksForNewProject(projectFullPath, checkContext);
        }
 
        public void ProcessProjectEvaluationStarted(
            ICheckContext checkContext,
            string projectFullPath)
        {
            _projectsByEvaluationId[checkContext.BuildEventContext.EvaluationId] = projectFullPath;
            _deferredProjectEvalIdToImportedProjects.TryAdd(checkContext.BuildEventContext.EvaluationId, [projectFullPath]);
        }
 
        /*
         *
         * Following methods are for future use (should we decide to approach in-execution check)
         *
         */
 
        public void EndProjectEvaluation(BuildEventContext buildEventContext)
        {
        }
 
        public void StartProjectRequest(ICheckContext checkContext, string projectFullPath)
        {
            BuildEventContext buildEventContext = checkContext.BuildEventContext;
 
            // There can be multiple ProjectStarted-ProjectFinished per single configuration project build (each request for different target)
            _projectsByInstanceId[buildEventContext.ProjectInstanceId] = projectFullPath;
 
            if (_deferredEvalDiagnostics.TryGetValue(buildEventContext.EvaluationId, out var list))
            {
                foreach (BuildEventArgs deferredArgs in list)
                {
                    deferredArgs.BuildEventContext = deferredArgs.BuildEventContext!.WithInstanceIdAndContextId(buildEventContext);
                    checkContext.DispatchBuildEvent(deferredArgs);
                }
                list.Clear();
                _deferredEvalDiagnostics.Remove(buildEventContext.EvaluationId);
            }
        }
 
        private readonly Dictionary<int, List<BuildEventArgs>> _deferredEvalDiagnostics = new();
 
        /// <summary>
        /// Propagates a newly imported project file to all projects that import the original project file.
        /// This method ensures that if Project A imports Project B, and Project B now imports Project C,
        /// then Project A will also show Project C as an import.
        /// </summary>
        /// <param name="evaluationId">The evaluation id is associated with the root project path.</param>
        /// <param name="originalProjectFile">The path of the project file that is importing a new project.</param>
        /// <param name="newImportedProjectFile">The path of the newly imported project file.</param>
        private void PropagateImport(int evaluationId, string originalProjectFile, string newImportedProjectFile)
        {
            if (_deferredProjectEvalIdToImportedProjects.TryGetValue(evaluationId, out HashSet<string>? importedProjects))
            {
                lock (importedProjects)
                {
                    if (importedProjects.Contains(originalProjectFile))
                    {
                        importedProjects.Add(newImportedProjectFile);
                    }
                }
            }
        }
 
        void IResultReporter.ReportResult(BuildEventArgs eventArgs, ICheckContext checkContext)
        {
            // If we do not need to decide on promotability/demotability of warnings or we are ready to decide on those
            //  - we can just dispatch the event.
            if (
                // no context - we cannot defer as we'd need eval id to queue it
                eventArgs.BuildEventContext == null ||
                // no eval id - we cannot defer as we'd need eval id to queue it
                eventArgs.BuildEventContext.EvaluationId == BuildEventContext.InvalidEvaluationId ||
                // instance id known - no need to defer
                eventArgs.BuildEventContext.ProjectInstanceId != BuildEventContext.InvalidProjectInstanceId ||
                // it's not a warning - no need to defer
                eventArgs is not BuildWarningEventArgs)
            {
                checkContext.DispatchBuildEvent(eventArgs);
                return;
            }
 
            // This is evaluation - so we need to defer it until we know the instance id and context id
 
            if (!_deferredEvalDiagnostics.TryGetValue(eventArgs.BuildEventContext.EvaluationId, out var list))
            {
                list = [];
                _deferredEvalDiagnostics[eventArgs.BuildEventContext.EvaluationId] = list;
            }
 
            list.Add(eventArgs);
        }
 
        public void EndProjectRequest(
            ICheckContext checkContext,
            string projectFullPath)
        {
            _buildEventsProcessor.ProcessProjectDone(checkContext, projectFullPath);
        }
 
        public void ProcessPropertyRead(PropertyReadInfo propertyReadInfo, CheckLoggingContext checkContext)
        {
            if (!_buildCheckCentralContext.HasPropertyReadActions)
            {
                return;
            }
 
            if (TryGetProjectFullPath(checkContext.BuildEventContext, out string projectFullPath))
            {
                PropertyReadData propertyReadData = new(
                    projectFullPath,
                    checkContext.BuildEventContext.ProjectInstanceId,
                    propertyReadInfo);
                _buildEventsProcessor.ProcessPropertyRead(propertyReadData, checkContext);
            }
        }
 
        public void ProcessPropertyWrite(PropertyWriteInfo propertyWriteInfo, CheckLoggingContext checkContext)
        {
            if (!_buildCheckCentralContext.HasPropertyWriteActions)
            {
                return;
            }
 
            if (TryGetProjectFullPath(checkContext.BuildEventContext, out string projectFullPath))
            {
                PropertyWriteData propertyWriteData = new(
                    projectFullPath,
                    checkContext.BuildEventContext.ProjectInstanceId,
                    propertyWriteInfo);
                _buildEventsProcessor.ProcessPropertyWrite(propertyWriteData, checkContext);
            }
        }
 
        public void Shutdown()
        { /* Too late here for any communication to the main node or for logging anything */ }
 
        private class CheckFactoryContext(
            CheckFactory factory,
            string[] ruleIds,
            bool isEnabledByDefault)
        {
            public Check Factory()
            {
                Check ba = factory();
                return ba;
            }
 
            public CheckWrapper Initialize(Check ba, IResultReporter resultReporter, ConfigurationContext configContext)
            {
                try
                {
                    ba.Initialize(configContext);
                }
                catch (BuildCheckConfigurationException)
                {
                    throw;
                }
                catch (Exception e)
                {
                    throw new BuildCheckConfigurationException(
                        $"The Check '{ba.FriendlyName}' failed to initialize: {e.Message}", e);
                }
                return new CheckWrapper(ba, resultReporter);
            }
 
            public CheckWrapper? MaterializedCheck { get; set; }
 
            public string[] RuleIds { get; init; } = ruleIds;
 
            public bool IsEnabledByDefault { get; init; } = isEnabledByDefault;
 
            public string FriendlyName => MaterializedCheck?.Check.FriendlyName ?? factory().FriendlyName;
        }
    }
}
 
internal interface IResultReporter
{
    void ReportResult(BuildEventArgs result, ICheckContext checkContext);
}