| File: BuildCheck\Infrastructure\CheckWrapper.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.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(); } }