|
// 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 Microsoft.Build.Framework;
namespace Microsoft.Build.Experimental.BuildCheck.Infrastructure;
/// <summary>
/// A wrapping, enriching class for BuildCheck - so that we have additional data and functionality.
/// </summary>
internal sealed class CheckWrapper
{
private readonly Stopwatch _stopwatch = new Stopwatch();
private readonly BuildCheckRuleTelemetryData[] _ruleTelemetryData;
/// <summary>
/// Maximum amount of messages that could be sent per check rule.
/// </summary>
public const int MaxReportsNumberPerRule = 20;
/// <summary>
/// Keeps track of number of reports sent per rule.
/// </summary>
private int _reportsCount = 0;
/// <summary>
/// Flags that this check should no more used and be deregistered.
/// </summary>
public bool IsThrottled { get; private set; } = false;
/// <summary>
/// Whether to limit number of reports for the Check.
/// </summary>
private readonly bool _limitReportsNumber = !Traits.Instance.EscapeHatches.DoNotLimitBuildCheckResultsNumber;
private readonly IResultReporter _resultReporter;
public CheckWrapper(Check check, IResultReporter resultReporter)
{
Check = check;
_resultReporter = resultReporter;
_ruleTelemetryData = new BuildCheckRuleTelemetryData[check.SupportedRules.Count];
InitializeTelemetryData(_ruleTelemetryData, check);
}
private static void InitializeTelemetryData(BuildCheckRuleTelemetryData[] ruleTelemetryData, Check check)
{
int idx = 0;
foreach (CheckRule checkRule in check.SupportedRules)
{
ruleTelemetryData[idx++] = new BuildCheckRuleTelemetryData(
ruleId: checkRule.Id,
checkFriendlyName: check.FriendlyName,
isBuiltIn: check.IsBuiltIn,
defaultSeverity: (checkRule.DefaultConfiguration.Severity ??
CheckConfigurationEffective.Default.Severity).ToDiagnosticSeverity());
}
}
internal Check Check { get; }
private bool _areStatsInitialized = false;
// Let's optimize for the scenario where users have a single .editorconfig file that applies to the whole solution.
// In such case - configuration will be same for all projects. So we do not need to store it per project in a collection.
internal CheckConfigurationEffective? CommonConfig { get; private set; }
/// <summary>
/// Ensures the check being configured for a new project (as each project can have different settings)
/// </summary>
/// <param name="fullProjectPath"></param>
/// <param name="effectiveConfigs">Resulting merged configurations per rule (merged from check default and explicit user editorconfig).</param>
/// <param name="editorConfigs">Configurations from editorconfig per rule.</param>
internal void StartNewProject(
string fullProjectPath,
IReadOnlyList<CheckConfigurationEffective> effectiveConfigs,
IReadOnlyList<CheckConfiguration> editorConfigs)
{
// Let's first update the telemetry data for the rules.
int idx = 0;
foreach (BuildCheckRuleTelemetryData ruleTelemetryData in _ruleTelemetryData)
{
CheckConfigurationEffective effectiveConfig = effectiveConfigs[Math.Max(idx, effectiveConfigs.Count - 1)];
if (editorConfigs[idx].Severity != null)
{
ruleTelemetryData.ExplicitSeverities.Add(editorConfigs[idx].Severity!.Value.ToDiagnosticSeverity());
}
if (effectiveConfig.IsEnabled)
{
ruleTelemetryData.ProjectNamesWhereEnabled.Add(fullProjectPath);
}
idx++;
}
if (!_areStatsInitialized)
{
_areStatsInitialized = true;
CommonConfig = effectiveConfigs[0];
if (effectiveConfigs.Count == 1)
{
return;
}
}
// The Common configuration is not common anymore - let's nullify it and we will need to fetch configuration per project.
if (CommonConfig == null || !effectiveConfigs.All(t => t.IsSameConfigurationAs(CommonConfig)))
{
CommonConfig = null;
}
}
private void AddDiagnostic(CheckConfigurationEffective configurationEffective)
{
BuildCheckRuleTelemetryData? telemetryData =
_ruleTelemetryData.FirstOrDefault(td => td.RuleId.Equals(configurationEffective.RuleId));
if (telemetryData == null)
{
return;
}
switch (configurationEffective.Severity)
{
case CheckResultSeverity.Suggestion:
telemetryData.IncrementMessagesCount();
break;
case CheckResultSeverity.Warning:
telemetryData.IncrementWarningsCount();
break;
case CheckResultSeverity.Error:
telemetryData.IncrementErrorsCount();
break;
case CheckResultSeverity.Default:
case CheckResultSeverity.None:
default:
break;
}
if (IsThrottled)
{
telemetryData.SetThrottled();
}
}
internal void ReportResult(BuildCheckResult result, ICheckContext checkContext, CheckConfigurationEffective config)
{
if (!IsThrottled)
{
_reportsCount++;
BuildEventArgs eventArgs = result.ToEventArgs(config.Severity);
eventArgs.BuildEventContext = checkContext.BuildEventContext;
_resultReporter.ReportResult(eventArgs, checkContext);
// Big amount of build check messages may lead to build hang.
// See issue https://github.com/dotnet/msbuild/issues/10414
// As a temporary fix, we will limit the number of messages that could be reported by the check.
if (_limitReportsNumber)
{
if (_reportsCount >= MaxReportsNumberPerRule)
{
IsThrottled = true;
}
}
// Add the diagnostic to the check wrapper for telemetry purposes.
AddDiagnostic(config);
}
}
// to be used on eval node (BuildCheckDataSource.check)
internal void UninitializeStats()
{
_areStatsInitialized = false;
}
internal IReadOnlyList<BuildCheckRuleTelemetryData> GetRuleTelemetryData()
{
foreach (BuildCheckRuleTelemetryData ruleTelemetryData in _ruleTelemetryData)
{
ruleTelemetryData.TotalRuntime = _stopwatch.Elapsed;
}
return _ruleTelemetryData;
}
internal CleanupScope StartSpan()
{
_stopwatch.Start();
return new CleanupScope(_stopwatch.Stop);
}
internal readonly struct CleanupScope(Action disposeAction) : IDisposable
{
public void Dispose() => disposeAction();
}
}
|