File: BuildCheck\Infrastructure\BuildEventsProcessor.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.Generic;
using System.Linq;
using Microsoft.Build.Construction;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
 
namespace Microsoft.Build.Experimental.BuildCheck.Infrastructure;
 
internal class BuildEventsProcessor(BuildCheckCentralContext buildCheckCentralContext)
{
    /// <summary>
    /// Represents a task currently being executed.
    /// </summary>
    /// <remarks>
    /// <see cref="TaskParameters"/> is stored in its own field typed as a mutable dictionary because <see cref="CheckData"/>
    /// is immutable.
    /// </remarks>
    private struct ExecutingTaskData
    {
        public TaskInvocationCheckData CheckData;
        public Dictionary<string, TaskInvocationCheckData.TaskParameter> TaskParameters;
    }
 
    /// <summary>
    /// Uniquely identifies a task.
    /// </summary>
    private record struct TaskKey(int ProjectContextId, int TargetId, int TaskId)
    {
        public TaskKey(BuildEventContext context)
            : this(context.ProjectContextId, context.TargetId, context.TaskId)
        { }
    }
 
    private readonly SimpleProjectRootElementCache _cache = new SimpleProjectRootElementCache();
    private readonly BuildCheckCentralContext _buildCheckCentralContext = buildCheckCentralContext;
 
    /// <summary>
    /// Keeps track of in-flight tasks. Keyed by task ID as passed in <see cref="BuildEventContext.TaskId"/>.
    /// </summary>
    private readonly Dictionary<TaskKey, ExecutingTaskData> _tasksBeingExecuted = [];
 
    internal static Dictionary<string, string> ExtractPropertiesLookup(ProjectEvaluationFinishedEventArgs evaluationFinishedEventArgs)
    {
        Dictionary<string, string> propertiesLookup = new Dictionary<string, string>();
        Internal.Utilities.EnumerateProperties(evaluationFinishedEventArgs.Properties, propertiesLookup,
            static (dict, kvp) => dict.Add(kvp.Key, kvp.Value));
 
        return propertiesLookup;
    }
 
    // This requires MSBUILDLOGPROPERTIESANDITEMSAFTEREVALUATION set to 1
    internal void ProcessEvaluationFinishedEventArgs(
        ICheckContext checkContext,
        ProjectEvaluationFinishedEventArgs evaluationFinishedEventArgs,
        Dictionary<string, string>? propertiesLookup)
    {
        if (_buildCheckCentralContext.HasEvaluatedPropertiesActions)
        {
            propertiesLookup ??= ExtractPropertiesLookup(evaluationFinishedEventArgs);
 
            EvaluatedPropertiesCheckData checkData =
                new(evaluationFinishedEventArgs.ProjectFile!,
                    evaluationFinishedEventArgs.BuildEventContext?.ProjectInstanceId,
                    propertiesLookup!);
 
            _buildCheckCentralContext.RunEvaluatedPropertiesActions(checkData, checkContext, ReportResult);
        }
 
        if (_buildCheckCentralContext.HasParsedItemsActions)
        {
            ProjectRootElement xml = ProjectRootElement.OpenProjectOrSolution(
                evaluationFinishedEventArgs.ProjectFile!, /*unused*/
                null, /*unused*/null, _cache, false /*Not explicitly loaded - unused*/);
 
            ParsedItemsCheckData itemsCheckData = new(
                evaluationFinishedEventArgs.ProjectFile!,
                evaluationFinishedEventArgs.BuildEventContext?.ProjectInstanceId,
                new ItemsHolder(xml.Items, xml.ItemGroups));
 
            _buildCheckCentralContext.RunParsedItemsActions(itemsCheckData, checkContext, ReportResult);
        }
    }
 
    /// <summary>
    /// The method collects events associated with the used environment variables in projects.
    /// </summary>
    internal void ProcessEnvironmentVariableReadEventArgs(ICheckContext checkContext, string projectPath, string envVarKey, string envVarValue, IElementLocation elementLocation)
    {
        EnvironmentVariableCheckData checkData = new(projectPath, checkContext.BuildEventContext?.ProjectInstanceId, envVarKey, envVarValue, elementLocation);
 
        _buildCheckCentralContext.RunEnvironmentVariableActions(checkData, checkContext, ReportResult);
    }
 
    /// <summary>
    /// The method handles events associated with the ProjectImportedEventArgs.
    /// </summary>
    internal void ProcessProjectImportedEventArgs(ICheckContext checkContext, string projectPath, string importedProjectPath)
    {
        ProjectImportedCheckData checkData = new(importedProjectPath, projectPath, checkContext.BuildEventContext?.ProjectInstanceId);
 
        _buildCheckCentralContext.RunProjectImportedActions(checkData, checkContext, ReportResult);
    }
 
    internal void ProcessBuildDone(ICheckContext checkContext)
    {
        if (!_buildCheckCentralContext.HasBuildFinishedActions)
        {
            // No analyzer is interested in the event -> nothing to do.
            return;
        }
 
        _buildCheckCentralContext.RunBuildFinishedActions(new BuildFinishedCheckData(), checkContext, ReportResult);
    }
 
    internal void ProcessTaskStartedEventArgs(
        ICheckContext checkContext,
        TaskStartedEventArgs taskStartedEventArgs)
    {
        if (!_buildCheckCentralContext.HasTaskInvocationActions)
        {
            // No check is interested in task invocation actions -> nothing to do.
            return;
        }
 
        if (taskStartedEventArgs.BuildEventContext is not null)
        {
            ElementLocation invocationLocation = ElementLocation.Create(
                taskStartedEventArgs.TaskFile,
                taskStartedEventArgs.LineNumber,
                taskStartedEventArgs.ColumnNumber);
 
            // Add a new entry to _tasksBeingExecuted. TaskParameters are initialized empty and will be recorded
            // based on TaskParameterEventArgs we receive later.
            Dictionary<string, TaskInvocationCheckData.TaskParameter> taskParameters = new();
 
            ExecutingTaskData taskData = new()
            {
                TaskParameters = taskParameters,
                CheckData = new(
                    projectFilePath: taskStartedEventArgs.ProjectFile!,
                    projectConfigurationId: taskStartedEventArgs.BuildEventContext.ProjectInstanceId,
                    taskInvocationLocation: invocationLocation,
                    taskName: taskStartedEventArgs.TaskName,
                    taskAssemblyLocation: taskStartedEventArgs.TaskAssemblyLocation,
                    parameters: taskParameters),
            };
 
            _tasksBeingExecuted.Add(new TaskKey(taskStartedEventArgs.BuildEventContext), taskData);
        }
    }
 
    internal void ProcessTaskFinishedEventArgs(
        ICheckContext checkContext,
        TaskFinishedEventArgs taskFinishedEventArgs)
    {
        if (!_buildCheckCentralContext.HasTaskInvocationActions)
        {
            // No check is interested in task invocation actions -> nothing to do.
            return;
        }
 
        if (taskFinishedEventArgs?.BuildEventContext is not null)
        {
            TaskKey taskKey = new TaskKey(taskFinishedEventArgs.BuildEventContext);
            if (_tasksBeingExecuted.TryGetValue(taskKey, out ExecutingTaskData taskData))
            {
                // All task parameters have been recorded by now so remove the task from the dictionary and fire the registered build check actions.
                _tasksBeingExecuted.Remove(taskKey);
                _buildCheckCentralContext.RunTaskInvocationActions(taskData.CheckData, checkContext, ReportResult);
            }
        }
    }
 
    internal void ProcessTaskParameterEventArgs(
        ICheckContext checkContext,
        TaskParameterEventArgs taskParameterEventArgs)
    {
        if (!_buildCheckCentralContext.HasTaskInvocationActions)
        {
            // No check is interested in task invocation actions -> nothing to do.
            return;
        }
 
        bool isOutput;
        switch (taskParameterEventArgs.Kind)
        {
            case TaskParameterMessageKind.TaskInput: isOutput = false; break;
            case TaskParameterMessageKind.TaskOutput: isOutput = true; break;
            default: return;
        }
 
        if (taskParameterEventArgs.BuildEventContext is not null &&
            _tasksBeingExecuted.TryGetValue(new TaskKey(taskParameterEventArgs.BuildEventContext), out ExecutingTaskData taskData))
        {
            // Add the parameter name and value to the matching entry in _tasksBeingExecuted. Parameters come typed as IList
            // but it's more natural to pass them as scalar values so we unwrap one-element lists.
            string parameterName = taskParameterEventArgs.ParameterName;
            object? parameterValue = taskParameterEventArgs.Items?.Count switch
            {
                1 => taskParameterEventArgs.Items[0],
                _ => taskParameterEventArgs.Items,
            };
 
            taskData.TaskParameters[parameterName] = new TaskInvocationCheckData.TaskParameter(parameterValue, isOutput);
        }
    }
 
    public void ProcessPropertyRead(PropertyReadData propertyReadData, CheckLoggingContext checkContext)
        => _buildCheckCentralContext.RunPropertyReadActions(
                propertyReadData,
                checkContext,
                ReportResult);
 
    public void ProcessPropertyWrite(PropertyWriteData propertyWriteData, CheckLoggingContext checkContext)
        => _buildCheckCentralContext.RunPropertyWriteActions(
                propertyWriteData,
                checkContext,
                ReportResult);
 
    public void ProcessProjectDone(ICheckContext checkContext, string projectFullPath)
        => _buildCheckCentralContext.RunProjectProcessingDoneActions(
                new ProjectRequestProcessingDoneData(projectFullPath, checkContext.BuildEventContext.ProjectInstanceId),
                checkContext,
                ReportResult);
 
    private static void ReportResult(
        CheckWrapper checkWrapper,
        ICheckContext checkContext,
        CheckConfigurationEffective[] configPerRule,
        BuildCheckResult result)
    {
        if (!checkWrapper.Check.SupportedRules.Contains(result.CheckRule))
        {
            checkContext.DispatchAsErrorFromText(null, null, null,
                BuildEventFileInfo.Empty,
                $"The check '{checkWrapper.Check.FriendlyName}' reported a result for a rule '{result.CheckRule.Id}' that it does not support.");
            return;
        }
 
        CheckConfigurationEffective config = configPerRule.Length == 1
            ? configPerRule[0]
            : configPerRule.First(r =>
                r.RuleId.Equals(result.CheckRule.Id, StringComparison.CurrentCultureIgnoreCase));
 
        if (!config.IsEnabled)
        {
            return;
        }
 
        checkWrapper.ReportResult(result, checkContext, config);
    }
}