|
// 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.Diagnostics;
using System.Linq;
using System.Threading;
using Microsoft.Build.BackEnd;
using Microsoft.Build.BackEnd.Logging;
using Microsoft.Build.Experimental.BuildCheck.Acquisition;
using Microsoft.Build.Experimental.BuildCheck.Analyzers;
using Microsoft.Build.Experimental.BuildCheck.Logging;
using Microsoft.Build.Experimental.BuildCheck;
using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
namespace Microsoft.Build.Experimental.BuildCheck.Infrastructure;
internal delegate BuildAnalyzer BuildAnalyzerFactory();
internal delegate BuildAnalyzerWrapper BuildAnalyzerWrapperFactory(ConfigurationContext configurationContext);
/// <summary>
/// The central manager for the BuildCheck - this is the integration point with MSBuild infrastructure.
/// </summary>
internal sealed class BuildCheckManagerProvider : IBuildCheckManagerProvider
{
private static IBuildCheckManager? s_globalInstance;
internal static IBuildCheckManager GlobalInstance => s_globalInstance ?? throw new InvalidOperationException("BuildCheckManagerProvider not initialized");
public IBuildCheckManager Instance => GlobalInstance;
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 (s_globalInstance == null)
{
IBuildCheckManager instance;
if (host!.BuildParameters.IsBuildCheckEnabled)
{
instance = new BuildCheckManager(host.LoggingService);
}
else
{
instance = new NullBuildCheckManager();
}
// We are fine with the possibility of double creation here - as the construction is cheap
// and without side effects and the actual backing field is effectively immutable after the first assignment.
Interlocked.CompareExchange(ref s_globalInstance, instance, null);
}
}
public void ShutdownComponent() => GlobalInstance.Shutdown();
internal sealed class BuildCheckManager : IBuildCheckManager
{
private readonly TracingReporter _tracingReporter = new TracingReporter();
private readonly ConfigurationProvider _configurationProvider = new ConfigurationProvider();
private readonly BuildCheckCentralContext _buildCheckCentralContext;
private readonly ILoggingService _loggingService;
private readonly List<BuildAnalyzerFactoryContext> _analyzersRegistry;
private readonly bool[] _enabledDataSources = new bool[(int)BuildCheckDataSource.ValuesCount];
private readonly BuildEventsProcessor _buildEventsProcessor;
private readonly IBuildCheckAcquisitionModule _acquisitionModule;
internal BuildCheckManager(ILoggingService loggingService)
{
_analyzersRegistry = new List<BuildAnalyzerFactoryContext>();
_acquisitionModule = new BuildCheckAcquisitionModule(loggingService);
_loggingService = loggingService;
_buildCheckCentralContext = new(_configurationProvider);
_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 analyzers 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;
RegisterBuiltInAnalyzers(buildCheckDataSource);
}
stopwatch.Stop();
_tracingReporter.AddSetDataSourceStats(stopwatch.Elapsed);
}
public void ProcessAnalyzerAcquisition(AnalyzerAcquisitionData acquisitionData, BuildEventContext buildEventContext)
{
Stopwatch stopwatch = Stopwatch.StartNew();
if (IsInProcNode)
{
var analyzersFactories = _acquisitionModule.CreateBuildAnalyzerFactories(acquisitionData, buildEventContext);
if (analyzersFactories.Count != 0)
{
RegisterCustomAnalyzer(BuildCheckDataSource.EventArgs, analyzersFactories, buildEventContext);
}
else
{
_loggingService.LogComment(buildEventContext, MessageImportance.Normal, "CustomAnalyzerFailedAcquisition", acquisitionData.AssemblyPath);
}
}
else
{
BuildCheckAcquisitionEventArgs eventArgs = acquisitionData.ToBuildEventArgs();
eventArgs.BuildEventContext = buildEventContext;
_loggingService.LogBuildEvent(eventArgs);
}
stopwatch.Stop();
_tracingReporter.AddAcquisitionStats(stopwatch.Elapsed);
}
private static T Construct<T>() where T : new() => new();
private static readonly (string[] ruleIds, bool defaultEnablement, BuildAnalyzerFactory factory)[][] s_builtInFactoriesPerDataSource =
[
// BuildCheckDataSource.EventArgs
[
([SharedOutputPathAnalyzer.SupportedRule.Id], SharedOutputPathAnalyzer.SupportedRule.DefaultConfiguration.IsEnabled ?? false, Construct<SharedOutputPathAnalyzer>),
([DoubleWritesAnalyzer.SupportedRule.Id], DoubleWritesAnalyzer.SupportedRule.DefaultConfiguration.IsEnabled ?? false, Construct<DoubleWritesAnalyzer>),
],
// BuildCheckDataSource.Execution
[]
];
/// <summary>
/// For tests only. TODO: Remove when analyzer acquisition is done.
/// </summary>
internal static (string[] ruleIds, bool defaultEnablement, BuildAnalyzerFactory factory)[][]? s_testFactoriesPerDataSource;
private void RegisterBuiltInAnalyzers(BuildCheckDataSource buildCheckDataSource)
{
_analyzersRegistry.AddRange(
s_builtInFactoriesPerDataSource[(int)buildCheckDataSource]
.Select(v => new BuildAnalyzerFactoryContext(v.factory, v.ruleIds, v.defaultEnablement)));
if (s_testFactoriesPerDataSource is not null)
{
_analyzersRegistry.AddRange(
s_testFactoriesPerDataSource[(int)buildCheckDataSource]
.Select(v => new BuildAnalyzerFactoryContext(v.factory, v.ruleIds, v.defaultEnablement)));
}
}
/// <summary>
/// To be used by acquisition module.
/// Registers the custom analyzers, the construction of analyzers is deferred until the first using project is encountered.
/// </summary>
internal void RegisterCustomAnalyzers(
BuildCheckDataSource buildCheckDataSource,
IEnumerable<BuildAnalyzerFactory> factories,
string[] ruleIds,
bool defaultEnablement)
{
if (_enabledDataSources[(int)buildCheckDataSource])
{
foreach (BuildAnalyzerFactory factory in factories)
{
_analyzersRegistry.Add(new BuildAnalyzerFactoryContext(factory, ruleIds, defaultEnablement));
}
}
}
/// <summary>
/// To be used by acquisition module
/// Registers the custom analyzer, the construction of analyzer is needed during registration.
/// </summary>
/// <param name="buildCheckDataSource">Represents different data sources used in build check operations.</param>
/// <param name="factories">A collection of build analyzer factories for rules instantiation.</param>
/// <param name="buildEventContext">The context of the build event.</param>
internal void RegisterCustomAnalyzer(
BuildCheckDataSource buildCheckDataSource,
IEnumerable<BuildAnalyzerFactory> factories,
BuildEventContext buildEventContext)
{
if (_enabledDataSources[(int)buildCheckDataSource])
{
foreach (var factory in factories)
{
var instance = factory();
_analyzersRegistry.Add(new BuildAnalyzerFactoryContext(
factory,
instance.SupportedRules.Select(r => r.Id).ToArray(),
instance.SupportedRules.Any(r => r.DefaultConfiguration.IsEnabled == true)));
_loggingService.LogComment(buildEventContext, MessageImportance.Normal, "CustomAnalyzerSuccessfulAcquisition", instance.FriendlyName);
}
}
}
private void SetupSingleAnalyzer(BuildAnalyzerFactoryContext analyzerFactoryContext, string projectFullPath, BuildEventContext buildEventContext)
{
// For custom analyzers - 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 analyzer should not only post-filter results - it shouldn't even see the data
BuildAnalyzerWrapper wrapper;
BuildAnalyzerConfigurationInternal[] configurations;
if (analyzerFactoryContext.MaterializedAnalyzer == null)
{
BuildAnalyzerConfiguration[] userConfigs =
_configurationProvider.GetUserConfigurations(projectFullPath, analyzerFactoryContext.RuleIds);
if (userConfigs.All(c => !(c.IsEnabled ?? analyzerFactoryContext.IsEnabledByDefault)))
{
// the analyzer was not yet instantiated nor mounted - so nothing to do here now.
return;
}
CustomConfigurationData[] customConfigData =
_configurationProvider.GetCustomConfigurations(projectFullPath, analyzerFactoryContext.RuleIds);
ConfigurationContext configurationContext = ConfigurationContext.FromDataEnumeration(customConfigData);
wrapper = analyzerFactoryContext.Factory(configurationContext);
analyzerFactoryContext.MaterializedAnalyzer = wrapper;
BuildAnalyzer analyzer = wrapper.BuildAnalyzer;
// This is to facilitate possible perf improvement for custom analyzers - 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 (
analyzer.SupportedRules.Count != analyzerFactoryContext.RuleIds.Length
||
!analyzer.SupportedRules.Select(r => r.Id)
.SequenceEqual(analyzerFactoryContext.RuleIds, StringComparer.CurrentCultureIgnoreCase)
)
{
throw new BuildCheckConfigurationException(
$"The analyzer '{analyzer.FriendlyName}' exposes rules '{analyzer.SupportedRules.Select(r => r.Id).ToCsvString()}', but different rules were declared during registration: '{analyzerFactoryContext.RuleIds.ToCsvString()}'");
}
configurations = _configurationProvider.GetMergedConfigurations(userConfigs, analyzer);
// technically all analyzers 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);
var wrappedContext = new BuildCheckRegistrationContext(wrapper, _buildCheckCentralContext);
analyzer.RegisterActions(wrappedContext);
}
else
{
wrapper = analyzerFactoryContext.MaterializedAnalyzer;
configurations = _configurationProvider.GetMergedConfigurations(projectFullPath, wrapper.BuildAnalyzer);
_configurationProvider.CheckCustomConfigurationDataValidity(projectFullPath,
analyzerFactoryContext.RuleIds[0]);
// Update the wrapper
wrapper.StartNewProject(projectFullPath, configurations);
}
if (configurations.GroupBy(c => c.EvaluationAnalysisScope).Count() > 1)
{
throw new BuildCheckConfigurationException(
string.Format("All rules for a single analyzer should have the same EvaluationAnalysisScope for a single project (violating rules: [{0}], project: {1})",
analyzerFactoryContext.RuleIds.ToCsvString(),
projectFullPath));
}
}
private void SetupAnalyzersForNewProject(string projectFullPath, BuildEventContext buildEventContext)
{
// Only add analyzers here
// On an execution node - we might remove and dispose the analyzers once project is done
// If it's already constructed - just control the custom settings do not differ
Stopwatch stopwatch = Stopwatch.StartNew();
List<BuildAnalyzerFactoryContext> analyzersToRemove = new();
foreach (BuildAnalyzerFactoryContext analyzerFactoryContext in _analyzersRegistry)
{
try
{
SetupSingleAnalyzer(analyzerFactoryContext, projectFullPath, buildEventContext);
}
catch (BuildCheckConfigurationException e)
{
_loggingService.LogErrorFromText(buildEventContext, null, null, null,
new BuildEventFileInfo(projectFullPath),
e.Message);
analyzersToRemove.Add(analyzerFactoryContext);
}
}
analyzersToRemove.ForEach(c =>
{
_analyzersRegistry.Remove(c);
_loggingService.LogCommentFromText(buildEventContext, MessageImportance.High, $"Dismounting analyzer '{c.FriendlyName}'");
});
foreach (var analyzerToRemove in analyzersToRemove.Select(a => a.MaterializedAnalyzer).Where(a => a != null))
{
_buildCheckCentralContext.DeregisterAnalyzer(analyzerToRemove!);
_tracingReporter.AddAnalyzerStats(analyzerToRemove!.BuildAnalyzer.FriendlyName, analyzerToRemove.Elapsed);
analyzerToRemove.BuildAnalyzer.Dispose();
}
stopwatch.Stop();
_tracingReporter.AddNewProjectStats(stopwatch.Elapsed);
}
public void ProcessEvaluationFinishedEventArgs(
AnalyzerLoggingContext buildAnalysisContext,
ProjectEvaluationFinishedEventArgs evaluationFinishedEventArgs)
=> _buildEventsProcessor
.ProcessEvaluationFinishedEventArgs(buildAnalysisContext, evaluationFinishedEventArgs);
public void ProcessTaskStartedEventArgs(
AnalyzerLoggingContext buildAnalysisContext,
TaskStartedEventArgs taskStartedEventArgs)
=> _buildEventsProcessor
.ProcessTaskStartedEventArgs(buildAnalysisContext, taskStartedEventArgs);
public void ProcessTaskFinishedEventArgs(
AnalyzerLoggingContext buildAnalysisContext,
TaskFinishedEventArgs taskFinishedEventArgs)
=> _buildEventsProcessor
.ProcessTaskFinishedEventArgs(buildAnalysisContext, taskFinishedEventArgs);
public void ProcessTaskParameterEventArgs(
AnalyzerLoggingContext buildAnalysisContext,
TaskParameterEventArgs taskParameterEventArgs)
=> _buildEventsProcessor
.ProcessTaskParameterEventArgs(buildAnalysisContext, taskParameterEventArgs);
public Dictionary<string, TimeSpan> CreateAnalyzerTracingStats()
{
foreach (BuildAnalyzerFactoryContext analyzerFactoryContext in _analyzersRegistry)
{
if (analyzerFactoryContext.MaterializedAnalyzer != null)
{
_tracingReporter.AddAnalyzerStats(analyzerFactoryContext.FriendlyName,
analyzerFactoryContext.MaterializedAnalyzer.Elapsed);
analyzerFactoryContext.MaterializedAnalyzer.ClearStats();
}
}
_tracingReporter.AddAnalyzerInfraStats();
return _tracingReporter.TracingStats;
}
public void FinalizeProcessing(LoggingContext loggingContext)
{
if (IsInProcNode)
{
// We do not want to send tracing stats from in-proc node
return;
}
var analyzerEventStats = CreateAnalyzerTracingStats();
BuildCheckTracingEventArgs analyzerEventArg =
new(analyzerEventStats) { BuildEventContext = loggingContext.BuildEventContext };
loggingContext.LogBuildEvent(analyzerEventArg);
}
public void StartProjectEvaluation(BuildCheckDataSource buildCheckDataSource, BuildEventContext buildEventContext,
string fullPath)
{
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 BuildExecution source
// both in a single manager. The project started is first encountered by the execution before the EventArg is sent
return;
}
SetupAnalyzersForNewProject(fullPath, buildEventContext);
}
/*
*
* Following methods are for future use (should we decide to approach in-execution analysis)
*
*/
public void EndProjectEvaluation(BuildCheckDataSource buildCheckDataSource, BuildEventContext buildEventContext)
{
}
public void StartProjectRequest(BuildCheckDataSource buildCheckDataSource, BuildEventContext buildEventContext)
{
}
public void EndProjectRequest(BuildCheckDataSource buildCheckDataSource, BuildEventContext buildEventContext)
{
}
public void Shutdown()
{ /* Too late here for any communication to the main node or for logging anything */ }
private class BuildAnalyzerFactoryContext(
BuildAnalyzerFactory factory,
string[] ruleIds,
bool isEnabledByDefault)
{
public BuildAnalyzerWrapperFactory Factory { get; init; } = configContext =>
{
BuildAnalyzer ba = factory();
ba.Initialize(configContext);
return new BuildAnalyzerWrapper(ba);
};
public BuildAnalyzerWrapper? MaterializedAnalyzer { get; set; }
public string[] RuleIds { get; init; } = ruleIds;
public bool IsEnabledByDefault { get; init; } = isEnabledByDefault;
public string FriendlyName => MaterializedAnalyzer?.BuildAnalyzer.FriendlyName ?? factory().FriendlyName;
}
}
}
|