File: BuildCheck\Infrastructure\BuildCheckCentralContext.cs
Web Access
Project: src\msbuild\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.Generic;
using System.Linq;
using System.Threading;
using Microsoft.Build.BuildCheck.Infrastructure;
using Microsoft.Build.Shared;

namespace Microsoft.Build.Experimental.BuildCheck.Infrastructure;

/// <summary>
/// A manager of the runs of the checks - deciding based on configuration of what to run and what to postfilter.
/// </summary>
/// <remarks>
/// Callback registration and deregistration publish immutable snapshots. Dispatch uses the snapshot current at its start,
/// so an in-flight dispatch may invoke a callback after its check is deregistered. The <c>Has*Actions</c> properties are
/// advisory and callers must rely on dispatch snapshot semantics for correctness.
/// </remarks>
internal sealed class BuildCheckCentralContext
{
    private readonly IConfigurationProvider _configurationProvider;

    public BuildCheckCentralContext(
        IConfigurationProvider configurationProvider,
        Action<List<CheckWrapper>?, ICheckContext> removeCheck)
    {
        _configurationProvider = configurationProvider;
        _removeChecks = removeCheck;
    }

    private sealed class CallbackRegistry
    {
        internal CallbackCollection<EvaluatedPropertiesCheckData> EvaluatedPropertiesActions { get; } = new();
#pragma warning disable CS0618 // Type or member is obsolete
        internal CallbackCollection<ParsedItemsCheckData> ParsedItemsActions { get; } = new();
#pragma warning restore CS0618 // Type or member is obsolete
        internal CallbackCollection<EvaluatedItemsCheckData> EvaluatedItemsActions { get; } = new();
        internal CallbackCollection<TaskInvocationCheckData> TaskInvocationActions { get; } = new();
        internal CallbackCollection<PropertyReadData> PropertyReadActions { get; } = new();
        internal CallbackCollection<PropertyWriteData> PropertyWriteActions { get; } = new();
        internal CallbackCollection<ProjectRequestProcessingDoneData> ProjectRequestProcessingDoneActions { get; } = new();
        internal CallbackCollection<BuildFinishedCheckData> BuildFinishedActions { get; } = new();
        internal CallbackCollection<EnvironmentVariableCheckData> EnvironmentVariableCheckDataActions { get; } = new();
        internal CallbackCollection<ProjectImportedCheckData> ProjectImportedCheckDataActions { get; } = new();

        internal void DeregisterCheck(CheckWrapper check)
        {
            EvaluatedPropertiesActions.DeregisterCheck(check);
            ParsedItemsActions.DeregisterCheck(check);
            EvaluatedItemsActions.DeregisterCheck(check);
            TaskInvocationActions.DeregisterCheck(check);
            PropertyReadActions.DeregisterCheck(check);
            PropertyWriteActions.DeregisterCheck(check);
            ProjectRequestProcessingDoneActions.DeregisterCheck(check);
            BuildFinishedActions.DeregisterCheck(check);
            EnvironmentVariableCheckDataActions.DeregisterCheck(check);
            ProjectImportedCheckDataActions.DeregisterCheck(check);
        }
    }

    private sealed class CallbackCollection<T>
        where T : CheckData
    {
        private readonly object _writeLock = new();
        private (CheckWrapper, Action<BuildCheckDataContext<T>>)[] _callbacks = [];

        internal bool HasActions => Volatile.Read(ref _callbacks).Length > 0;

        internal (CheckWrapper, Action<BuildCheckDataContext<T>>)[] Snapshot => Volatile.Read(ref _callbacks);

        internal void Register(CheckWrapper check, Action<BuildCheckDataContext<T>> callback)
        {
            lock (_writeLock)
            {
                (CheckWrapper, Action<BuildCheckDataContext<T>>)[] callbacks = _callbacks;
                Array.Resize(ref callbacks, callbacks.Length + 1);
                callbacks[^1] = (check, callback);
                Volatile.Write(ref _callbacks, callbacks);
            }
        }

        internal void DeregisterCheck(CheckWrapper check)
        {
            lock (_writeLock)
            {
                (CheckWrapper, Action<BuildCheckDataContext<T>>)[] callbacks = _callbacks;
                if (callbacks.Any(callback => callback.Item1 == check))
                {
                    Volatile.Write(ref _callbacks, callbacks.Where(callback => callback.Item1 != check).ToArray());
                }
            }
        }
    }

    // In a future we can have callbacks per project as well
    private readonly CallbackRegistry _globalCallbacks = new();
    private readonly Action<List<CheckWrapper>?, ICheckContext> _removeChecks;


    // This we can potentially use to subscribe for receiving evaluated props in the
    //  build event args. However - this needs to be done early on, when checks might not be known yet
    internal bool HasEvaluatedPropertiesActions => _globalCallbacks.EvaluatedPropertiesActions.HasActions;

    internal bool HasParsedItemsActions => _globalCallbacks.ParsedItemsActions.HasActions;

    internal bool HasEvaluatedItemsActions => _globalCallbacks.EvaluatedItemsActions.HasActions;

    internal bool HasTaskInvocationActions => _globalCallbacks.TaskInvocationActions.HasActions;

    internal bool HasPropertyReadActions => _globalCallbacks.PropertyReadActions.HasActions;

    internal bool HasPropertyWriteActions => _globalCallbacks.PropertyWriteActions.HasActions;

    internal bool HasProjectRequestProcessingDoneActions => _globalCallbacks.ProjectRequestProcessingDoneActions.HasActions;

    internal bool HasBuildFinishedActions => _globalCallbacks.BuildFinishedActions.HasActions;

    internal bool HasEnvironmentVariableActions => _globalCallbacks.EnvironmentVariableCheckDataActions.HasActions;

    internal bool HasProjectImportedActions => _globalCallbacks.ProjectImportedCheckDataActions.HasActions;

    internal void RegisterEnvironmentVariableReadAction(CheckWrapper check, Action<BuildCheckDataContext<EnvironmentVariableCheckData>> environmentVariableAction)
       => RegisterAction(check, environmentVariableAction, _globalCallbacks.EnvironmentVariableCheckDataActions);

    internal void RegisterEvaluatedPropertiesAction(CheckWrapper check, Action<BuildCheckDataContext<EvaluatedPropertiesCheckData>> evaluatedPropertiesAction)
        // Here we might want to communicate to node that props need to be sent.
        //  (it was being communicated via MSBUILDLOGPROPERTIESANDITEMSAFTEREVALUATION)
        => RegisterAction(check, evaluatedPropertiesAction, _globalCallbacks.EvaluatedPropertiesActions);

#pragma warning disable CS0618 // Type or member is obsolete
    internal void RegisterParsedItemsAction(CheckWrapper check, Action<BuildCheckDataContext<ParsedItemsCheckData>> parsedItemsAction)
#pragma warning restore CS0618 // Type or member is obsolete
        => RegisterAction(check, parsedItemsAction, _globalCallbacks.ParsedItemsActions);

    internal void RegisterEvaluatedItemsAction(CheckWrapper check, Action<BuildCheckDataContext<EvaluatedItemsCheckData>> parsedItemsAction)
        => RegisterAction(check, parsedItemsAction, _globalCallbacks.EvaluatedItemsActions);

    internal void RegisterTaskInvocationAction(CheckWrapper check, Action<BuildCheckDataContext<TaskInvocationCheckData>> taskInvocationAction)
        => RegisterAction(check, taskInvocationAction, _globalCallbacks.TaskInvocationActions);

    internal void RegisterPropertyReadAction(CheckWrapper check, Action<BuildCheckDataContext<PropertyReadData>> propertyReadAction)
        => RegisterAction(check, propertyReadAction, _globalCallbacks.PropertyReadActions);

    internal void RegisterPropertyWriteAction(CheckWrapper check, Action<BuildCheckDataContext<PropertyWriteData>> propertyWriteAction)
        => RegisterAction(check, propertyWriteAction, _globalCallbacks.PropertyWriteActions);

    internal void RegisterProjectRequestProcessingDoneAction(CheckWrapper check, Action<BuildCheckDataContext<ProjectRequestProcessingDoneData>> projectDoneAction)
        => RegisterAction(check, projectDoneAction, _globalCallbacks.ProjectRequestProcessingDoneActions);

    internal void RegisterBuildFinishedAction(CheckWrapper check, Action<BuildCheckDataContext<BuildFinishedCheckData>> buildFinishedAction)
        => RegisterAction(check, buildFinishedAction, _globalCallbacks.BuildFinishedActions);

    internal void RegisterProjectImportedAction(CheckWrapper check, Action<BuildCheckDataContext<ProjectImportedCheckData>> projectImportedAction)
        => RegisterAction(check, projectImportedAction, _globalCallbacks.ProjectImportedCheckDataActions);

    private void RegisterAction<T>(
        CheckWrapper wrappedCheck,
        Action<BuildCheckDataContext<T>> handler,
        CallbackCollection<T> handlersRegistry)
        where T : CheckData
    {
        void WrappedHandler(BuildCheckDataContext<T> context)
        {
            using var _ = wrappedCheck.StartSpan();
            handler(context);
        }

        handlersRegistry.Register(wrappedCheck, WrappedHandler);
    }

    internal void DeregisterCheck(CheckWrapper check) => _globalCallbacks.DeregisterCheck(check);

    internal void RunEnvironmentVariableActions(
        EnvironmentVariableCheckData environmentVariableCheckData,
        ICheckContext checkContext,
        Action<CheckWrapper, ICheckContext, CheckConfigurationEffective[], BuildCheckResult>
            resultHandler)
        => RunRegisteredActions(_globalCallbacks.EnvironmentVariableCheckDataActions, environmentVariableCheckData, checkContext, resultHandler);

    internal void RunEvaluatedPropertiesActions(
        EvaluatedPropertiesCheckData evaluatedPropertiesCheckData,
        ICheckContext checkContext,
        Action<CheckWrapper, ICheckContext, CheckConfigurationEffective[], BuildCheckResult>
            resultHandler)
        => RunRegisteredActions(_globalCallbacks.EvaluatedPropertiesActions, evaluatedPropertiesCheckData, checkContext, resultHandler);

    internal void RunParsedItemsActions(
#pragma warning disable CS0618 // Type or member is obsolete
        ParsedItemsCheckData parsedItemsCheckData,
#pragma warning restore CS0618 // Type or member is obsolete
        ICheckContext checkContext,
        Action<CheckWrapper, ICheckContext, CheckConfigurationEffective[], BuildCheckResult>
            resultHandler)
        => RunRegisteredActions(_globalCallbacks.ParsedItemsActions, parsedItemsCheckData,
            checkContext, resultHandler);

    internal void RunEvaluatedItemsActions(
        EvaluatedItemsCheckData evaluatedItemsCheckData,
        ICheckContext checkContext,
        Action<CheckWrapper, ICheckContext, CheckConfigurationEffective[], BuildCheckResult>
            resultHandler)
        => RunRegisteredActions(_globalCallbacks.EvaluatedItemsActions, evaluatedItemsCheckData,
            checkContext, resultHandler);

    internal void RunTaskInvocationActions(
        TaskInvocationCheckData taskInvocationCheckData,
        ICheckContext checkContext,
        Action<CheckWrapper, ICheckContext, CheckConfigurationEffective[], BuildCheckResult>
            resultHandler)
        => RunRegisteredActions(_globalCallbacks.TaskInvocationActions, taskInvocationCheckData,
            checkContext, resultHandler);

    internal void RunPropertyReadActions(
        PropertyReadData propertyReadDataData,
        CheckLoggingContext checkContext,
        Action<CheckWrapper, ICheckContext, CheckConfigurationEffective[], BuildCheckResult>
            resultHandler)
        => RunRegisteredActions(_globalCallbacks.PropertyReadActions, propertyReadDataData,
            checkContext, resultHandler);

    internal void RunPropertyWriteActions(
        PropertyWriteData propertyWriteData,
        CheckLoggingContext checkContext,
        Action<CheckWrapper, ICheckContext, CheckConfigurationEffective[], BuildCheckResult>
            resultHandler)
        => RunRegisteredActions(_globalCallbacks.PropertyWriteActions, propertyWriteData,
            checkContext, resultHandler);

    internal void RunProjectProcessingDoneActions(
        ProjectRequestProcessingDoneData projectProcessingDoneData,
        ICheckContext checkContext,
        Action<CheckWrapper, ICheckContext, CheckConfigurationEffective[], BuildCheckResult>
            resultHandler)
        => RunRegisteredActions(_globalCallbacks.ProjectRequestProcessingDoneActions, projectProcessingDoneData,
            checkContext, resultHandler);

    internal void RunBuildFinishedActions(
        BuildFinishedCheckData buildFinishedCheckData,
        ICheckContext checkContext,
        Action<CheckWrapper, ICheckContext, CheckConfigurationEffective[], BuildCheckResult> resultHandler)
        => RunRegisteredActions(_globalCallbacks.BuildFinishedActions, buildFinishedCheckData, checkContext, resultHandler);

    internal void RunProjectImportedActions(
        ProjectImportedCheckData projectImportedCheckData,
        ICheckContext checkContext,
        Action<CheckWrapper, ICheckContext, CheckConfigurationEffective[], BuildCheckResult> resultHandler)
        => RunRegisteredActions(_globalCallbacks.ProjectImportedCheckDataActions, projectImportedCheckData, checkContext, resultHandler);

    private void RunRegisteredActions<T>(
        CallbackCollection<T> registeredCallbacks,
        T checkData,
        ICheckContext checkContext,
        Action<CheckWrapper, ICheckContext, CheckConfigurationEffective[], BuildCheckResult> resultHandler)
    where T : CheckData
    {
        string projectFullPath = checkData.ProjectFilePath;
        List<CheckWrapper>? checksToRemove = null;
        (CheckWrapper, Action<BuildCheckDataContext<T>>)[] callbacksSnapshot = registeredCallbacks.Snapshot;

        foreach (var checkCallback in callbacksSnapshot)
        {
            // Tracing - https://github.com/dotnet/msbuild/issues/9629 - we might want to account this entire block
            //  to the relevant check (with BuildCheckConfigurationEffective only the currently accounted part as being the 'core-execution' subspan)

            CheckConfigurationEffective? commonConfig = checkCallback.Item1.CommonConfig;
            CheckConfigurationEffective[] configPerRule;

            if (commonConfig != null)
            {
                if (!commonConfig.IsEnabled)
                {
                    continue;
                }

                configPerRule = [commonConfig];
            }
            else
            {
                configPerRule = _configurationProvider.GetMergedConfigurations(projectFullPath, checkCallback.Item1.Check);
                if (configPerRule.All(c => !c.IsEnabled))
                {
                    continue;
                }
            }

            // Here we might want to check the configPerRule[0].EvaluationsCheckScope - if the input data supports that
            // The decision and implementation depends on the outcome of the investigation tracked in:
            // https://github.com/orgs/dotnet/projects/373/views/1?pane=issue&itemId=57851137
            BuildCheckDataContext<T> context = new BuildCheckDataContext<T>(
                checkCallback.Item1,
                checkContext,
                configPerRule,
                resultHandler,
                checkData);

            try
            {
                checkCallback.Item2(context);
            }
            catch (Exception e)
            {
                checkContext.DispatchAsWarningFromText(
                    null,
                    null,
                    null,
                    new BuildEventFileInfo(projectFullPath),
                    $"The check '{checkCallback.Item1.Check.FriendlyName}' threw an exception while executing a registered action with message: {e.Message}");

                checksToRemove = checksToRemove ?? new List<CheckWrapper>();
                checksToRemove.Add(checkCallback.Item1);
            }
        }

        _removeChecks(checksToRemove, checkContext);
    }
}