docs/analyzers/DiagnosticSuppressorDesign.md
https://github.com/dotnet/roslyn/issues/30172: Programmatic suppression of warnings
Provide an ability for platform/library authors to author simple, context-aware compiler extensions to programmatically suppress specific instances of reported analyzer and/or compiler diagnostics, which are always known to be false positives in the context of the platform/library.
An analyzer/compiler diagnostic would be considered a candidate for programmatic suppression if all of the following conditions hold:
Let us consider the core example for UnityEngine covered in https://github.com/dotnet/roslyn/issues/30172:
using UnityEngine;
class BulletMovement : MonoBehaviour
{
[SerializeField] private float speed;
private void Update()
{
this.transform.position *= speed;
}
}
Serialization frameworks and tools embedding .NET Core or Mono often set the value of fields or call methods outside of user code. Such code is not available for analysis to the compiler and analyzers, leading to false reports such as following:
Field 'speed' is never assigned to, and will always have its default value 0info SPR0001: Diagnostic 'CS0649' was programmatically suppressed by a DiagnosticSuppressor with suppresion ID 'SPR1001' and justification 'Field symbols marked with SerializedFieldAttribute are implicitly assigned by UnityEngine at runtime and hence do not have its default value'
These diagnostics will always be logged in the msbuild binlog. They would not be visible in the console output for regular msbuild invocations, but increasing the msbuild verbosity to detailed or diagnostic will emit these diagnostics./nowarn:<%suppression_id%>: See image here for disabling the suppression ID from the project property page, which in turn generates the nowarn command line argument.Should the DiagnsoticSuppressor feature be Opt-in OR on-by-default: This was brought up few times in the past, especially with the concern with on-by-default behavior being that an end user would not see any indication in the command line that diagnostic suppressors were executed as part of their build. I think we have following options:
The draft PR for this feature currently puts the feature being a feature flag. If we decide to take the on-by-default route, I can revert the changes adding the feature flag. Otherwise, if we decide that we should make it opt-in with a new command line compiler switch, I would like to propose that we keep the feature flag for the initial PR, and then have a follow-up PR to add the command line switch.
Update: We have decided to keep the feature being a temporary feature flag for initial release, which will be removed once we are confident about the feature's performance, user experience, etc.
Message for the "Info" suppression diagnostic: I have chosen the below message format (with 3 format arguments), but any suggested changes are welcome:
Diagnostic 'CS0649' was programmatically suppressed by a DiagnosticSuppressor with suppresion ID 'SPR1001' and justification 'Field symbols marked with SerializedFieldAttribute are implicitly assigned by UnityEngine at runtime and hence do not have its default value'
What should be the diagnostic ID for the "Info" suppression diagnostic?: We have following options:
Update: We have decided the diagnostic ID would be SP0001.
https://github.com/dotnet/roslyn/pull/36067
namespace Microsoft.CodeAnalysis.Diagnostics
{
/// <summary>
/// The base type for diagnostic suppressors that can programmatically suppress analyzer and/or compiler non-error diagnostics.
/// </summary>
public abstract class DiagnosticSuppressor : DiagnosticAnalyzer
{
// Disallow suppressors from reporting diagnostics or registering analysis actions.
public sealed override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray<DiagnosticDescriptor>.Empty;
public sealed override void Initialize(AnalysisContext context) { }
/// <summary>
/// Returns a set of descriptors for the suppressions that this suppressor is capable of producing.
/// </summary>
public abstract ImmutableArray<SuppressionDescriptor> SupportedSuppressions { get; }
/// <summary>
/// Suppress analyzer and/or compiler non-error diagnostics reported for the compilation.
/// This may be a subset of the full set of reported diagnostics, as an optimization for
/// supporting incremental and partial analysis scenarios.
/// A diagnostic is considered suppressible by a DiagnosticSuppressor if *all* of the following conditions are met:
/// 1. Diagnostic is not already suppressed in source via pragma/suppress message attribute.
/// 2. Diagnostic's <see cref="Diagnostic.DefaultSeverity"/> is not <see cref="DiagnosticSeverity.Error"/>.
/// 3. Diagnostic is not tagged with <see cref="WellKnownDiagnosticTags.NotConfigurable"/> custom tag.
/// </summary>
public abstract void ReportSuppressions(SuppressionAnalysisContext context);
}
/// <summary>
/// Provides a description about a programmatic suppression of a <see cref="Diagnostic"/> by a <see cref="DiagnosticSuppressor"/>.
/// </summary>
public sealed class SuppressionDescriptor : IEquatable<SuppressionDescriptor>
{
/// <summary>
/// An unique identifier for the suppression.
/// </summary>
public string Id { get; }
/// <summary>
/// Identifier of the suppressed diagnostic, i.e. <see cref="Diagnostic.Id"/>.
/// </summary>
public string SuppressedDiagnosticId { get; }
/// <summary>
/// A localizable description about the suppression.
/// </summary>
public LocalizableString Description { get; }
}
/// <summary>
/// Context for suppressing analyzer and/or compiler non-error diagnostics reported for the compilation.
/// </summary>
public struct SuppressionAnalysisContext
{
/// <summary>
/// Suppressible analyzer and/or compiler non-error diagnostics reported for the compilation.
/// This may be a subset of the full set of reported diagnostics, as an optimization for
/// supporting incremental and partial analysis scenarios.
/// A diagnostic is considered suppressible by a DiagnosticSuppressor if *all* of the following conditions are met:
/// 1. Diagnostic is not already suppressed in source via pragma/suppress message attribute.
/// 2. Diagnostic's <see cref="Diagnostic.DefaultSeverity"/> is not <see cref="DiagnosticSeverity.Error"/>.
/// 3. Diagnostic is not tagged with <see cref="WellKnownDiagnosticTags.NotConfigurable"/> custom tag.
/// </summary>
public ImmutableArray<Diagnostic> ReportedDiagnostics { get; }
/// <summary>
/// Report a <see cref="Suppression"/> for a reported diagnostic.
/// </summary>
public void ReportSuppression(Suppression suppression);
/// <summary>
/// Gets a <see cref="SemanticModel"/> for the given <see cref="SyntaxTree"/>, which is shared across all analyzers.
/// </summary>
public SemanticModel GetSemanticModel(SyntaxTree syntaxTree);
/// <summary>
/// <see cref="CodeAnalysis.Compilation"/> for the context.
/// </summary>
public Compilation Compilation { get; }
/// <summary>
/// Options specified for the analysis.
/// </summary>
public AnalyzerOptions Options { get; }
/// <summary>
/// Token to check for requested cancellation of the analysis.
/// </summary>
public CancellationToken CancellationToken { get; }
}
/// <summary>
/// Programmatic suppression of a <see cref="Diagnostic"/> by a <see cref="DiagnosticSuppressor"/>.
/// </summary>
public struct Suppression
{
/// <summary>
/// Creates a suppression of a <see cref="Diagnostic"/> with the given <see cref="SuppressionDescriptor"/>.
/// </summary>
/// <param name="descriptor">
/// Descriptor for the suppression, which must be from <see cref="DiagnosticSuppressor.SupportedSuppressions"/>
/// for the <see cref="DiagnosticSuppressor"/> creating this suppression.
/// </param>
/// <param name="suppressedDiagnostic">
/// <see cref="Diagnostic"/> to be suppressed, which must be from <see cref="SuppressionAnalysisContext.ReportedDiagnostics"/>
/// for the suppression context in which this suppression is being created.</param>
public static Suppression Create(SuppressionDescriptor descriptor, Diagnostic suppressedDiagnostic);
/// <summary>
/// Descriptor for this suppression.
/// </summary>
public SuppressionDescriptor Descriptor { get; }
/// <summary>
/// Diagnostic suppressed by this suppression.
/// </summary>
public Diagnostic SuppressedDiagnostic { get; }
}
}