|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Xml;
using System.Xml.Serialization;
namespace Microsoft.DotNet.ApiCompatibility.Logging
{
/// <summary>
/// Suppression engine that contains a collection of <see cref="Suppression"/> items. It provides API to add a suppression, check if a passed-in suppression is already suppressed
/// and serialize all suppressions down into a file.
/// </summary>
/// <param name="noWarn">A string that contains warning and error codes to ignore suppressions with the corresponding diagnostic id.</param>
/// <param name="baselineAllErrors">If true, baselines all errors.</param>
public class SuppressionEngine(string? noWarn = null, bool baselineAllErrors = false) : ISuppressionEngine
{
protected const string DiagnosticIdDocumentationComment = " https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids ";
private readonly HashSet<Suppression> _baselineSuppressions = [];
private readonly HashSet<Suppression> _suppressions = [];
private readonly HashSet<string> _noWarn = string.IsNullOrEmpty(noWarn) ? [] : new(noWarn!.Split(';'), StringComparer.OrdinalIgnoreCase);
/// <inheritdoc/>
public bool BaselineAllErrors { get; } = baselineAllErrors;
/// <inheritdoc/>
public IReadOnlyCollection<Suppression> BaselineSuppressions => _baselineSuppressions;
/// <inheritdoc/>
public IReadOnlyCollection<Suppression> Suppressions => _suppressions;
/// <inheritdoc/>
public void LoadSuppressions(params string[] suppressionFiles)
{
XmlSerializer serializer = CreateXmlSerializer();
foreach (string suppressionFile in suppressionFiles)
{
try
{
using Stream reader = GetReadableStream(suppressionFile);
if (serializer.Deserialize(reader) is Suppression[] deserializedSuppressions)
{
_baselineSuppressions.UnionWith(deserializedSuppressions);
}
}
catch (FileNotFoundException) when (BaselineAllErrors)
{
// Throw if the passed in suppression file doesn't exist and errors aren't baselined.
}
}
}
/// <inheritdoc/>
public bool IsErrorSuppressed(Suppression error)
{
if (_noWarn.Contains(error.DiagnosticId) || _suppressions.Contains(error))
{
return true;
}
if (_baselineSuppressions.Contains(error))
{
AddSuppression(error);
return true;
}
// Only CP errors can have "global suppressions". Global suppressions are ones that could apply to more than just one compatibility difference.
if (error.DiagnosticId.StartsWith("cp", StringComparison.InvariantCultureIgnoreCase))
{
// - DiagnosticId, Target, IsBaselineSuppression
Suppression globalTargetSuppression = new(error.DiagnosticId, error.Target, isBaselineSuppression: error.IsBaselineSuppression);
// - Left, Right, IsBaselineSuppression
Suppression globalLeftRightSuppression = new(string.Empty, left: error.Left, right: error.Right, isBaselineSuppression: error.IsBaselineSuppression);
// - DiagnosticId, Left, Right, IsBaselineSuppression
Suppression globalDiagnosticIdLeftRightSuppression = new(error.DiagnosticId, left: error.Left, right: error.Right, isBaselineSuppression: error.IsBaselineSuppression);
if (_suppressions.Contains(globalTargetSuppression) ||
_suppressions.Contains(globalLeftRightSuppression) ||
_suppressions.Contains(globalDiagnosticIdLeftRightSuppression))
{
return true;
}
if (_baselineSuppressions.TryGetValue(globalTargetSuppression, out Suppression? globalSuppression) ||
_baselineSuppressions.TryGetValue(globalLeftRightSuppression, out globalSuppression) ||
_baselineSuppressions.TryGetValue(globalDiagnosticIdLeftRightSuppression, out globalSuppression))
{
AddSuppression(globalSuppression);
return true;
}
}
if (BaselineAllErrors)
{
AddSuppression(error);
return true;
}
return false;
}
/// <inheritdoc/>
public void AddSuppression(Suppression suppression) => _suppressions.Add(suppression);
/// <inheritdoc/>
public (bool SuppressionFileUpdated, IReadOnlyCollection<Suppression> UpdatedSuppressions)
WriteSuppressionsToFile(string suppressionOutputFile, bool preserveUnnecessarySuppressions = false)
{
// If unnecessary suppressions should be preserved in the suppression file, union the
// baseline suppressions with the set of actual suppressions. Duplicates are ignored.
HashSet<Suppression> suppressionsToSerialize = new(_suppressions);
if (preserveUnnecessarySuppressions)
{
suppressionsToSerialize.UnionWith(_baselineSuppressions);
}
// If there aren't any suppressions and baseline suppressions, skip writing the
// suppression file.
if (suppressionsToSerialize.Count == 0 && _baselineSuppressions.Count == 0)
{
return (false, []);
}
Suppression[] orderedSuppressions = suppressionsToSerialize
.OrderBy(suppression => suppression.DiagnosticId)
.ThenBy(suppression => suppression.Left)
.ThenBy(suppression => suppression.Right)
.ThenBy(suppression => suppression.Target)
.ToArray();
using Stream stream = GetWritableStream(suppressionOutputFile);
XmlWriter xmlWriter = XmlWriter.Create(stream, new XmlWriterSettings()
{
Encoding = Encoding.UTF8,
ConformanceLevel = ConformanceLevel.Document,
Indent = true
});
xmlWriter.WriteComment(DiagnosticIdDocumentationComment);
CreateXmlSerializer().Serialize(xmlWriter, orderedSuppressions);
AfterWritingSuppressionsCallback(stream);
return (true, orderedSuppressions);
}
/// <inheritdoc/>
public IReadOnlyCollection<Suppression> GetUnnecessarySuppressions() => _baselineSuppressions.Except(_suppressions).ToArray();
/// <inheritdoc/>
protected virtual void AfterWritingSuppressionsCallback(Stream stream) { /* Do nothing. Used for tests. */ }
// FileAccess.Read and FileShare.Read are specified to allow multiple processes to concurrently read from the suppression file.
/// <inheritdoc/>
protected virtual Stream GetReadableStream(string suppressionFile) => new FileStream(suppressionFile, FileMode.Open, FileAccess.Read, FileShare.Read);
/// <inheritdoc/>
protected virtual Stream GetWritableStream(string suppressionFile) => new FileStream(suppressionFile, FileMode.Create);
private static XmlSerializer CreateXmlSerializer() => new(typeof(Suppression[]), new XmlRootAttribute("Suppressions"));
}
}
|