From 276abf1cc57c494d51e40c4c0d8061845f7407fd Mon Sep 17 00:00:00 2001 From: Sam Harwell Date: Mon, 1 May 2023 09:20:12 -0500 Subject: [PATCH] Support testing suppressors for compiler diagnostics Fixes #1090 --- .../AnalyzerTest`1.cs | 49 +++++++++++-- .../Lightup/DiagnosticSuppressorWrapper.cs | 71 +++++++++++++++++++ .../Lightup/LightupHelpers.cs | 2 +- .../Lightup/SuppressionDescriptorWrapper.cs | 68 ++++++++++++++++++ .../DiagnosticSuppressorTests.cs | 43 +++++++++++ 5 files changed, 227 insertions(+), 6 deletions(-) create mode 100644 src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Lightup/DiagnosticSuppressorWrapper.cs create mode 100644 src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Lightup/SuppressionDescriptorWrapper.cs diff --git a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/AnalyzerTest`1.cs b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/AnalyzerTest`1.cs index 6684af1c..dc087ab0 100644 --- a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/AnalyzerTest`1.cs +++ b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/AnalyzerTest`1.cs @@ -1005,10 +1005,15 @@ namespace Microsoft.CodeAnalysis.Testing } } + private static bool IsCompilerDiagnosticId(string id) + { + return id.StartsWith("CS", StringComparison.Ordinal) + || id.StartsWith("BC", StringComparison.Ordinal); + } + private static bool IsSubjectToExclusion(DiagnosticResult result, ImmutableArray analyzers, (string filename, SourceText content)[] sources) { - if (result.Id.StartsWith("CS", StringComparison.Ordinal) - || result.Id.StartsWith("BC", StringComparison.Ordinal)) + if (IsCompilerDiagnosticId(result.Id)) { // This is a compiler diagnostic return false; @@ -1125,12 +1130,13 @@ namespace Microsoft.CodeAnalysis.Testing foreach (var project in solution.Projects) { var (compilation, generatorDiagnostics) = await GetProjectCompilationAsync(project, verifier, cancellationToken).ConfigureAwait(false); - var compilationWithAnalyzers = CreateCompilationWithAnalyzers(compilation, analyzers, GetAnalyzerOptions(project), cancellationToken); + var analyzerOptions = GetAnalyzerOptions(project); + var compilationWithAnalyzers = CreateCompilationWithAnalyzers(compilation, analyzers, analyzerOptions, cancellationToken); ImmutableArray allDiagnostics; if (AnalysisResultWrapper.WrappedType is not null) { - var compilationDiagnostics = compilation.GetDiagnostics(cancellationToken); + var compilerReportedDiagnostics = await GetCompilerDiagnosticsAsync(this, compilation, analyzers, analyzerOptions, cancellationToken).ConfigureAwait(false); var analysisResult = await compilationWithAnalyzers.GetAnalysisResultAsync(cancellationToken).ConfigureAwait(false); foreach (var (analyzer, analyzerNonLocalDiagnostics) in analysisResult.CompilationDiagnostics) { @@ -1140,7 +1146,7 @@ namespace Microsoft.CodeAnalysis.Testing } } - allDiagnostics = compilationDiagnostics.AddRange(analysisResult.GetAllDiagnostics()); + allDiagnostics = compilerReportedDiagnostics.AddRange(analysisResult.GetAllDiagnostics()); } else { @@ -1154,6 +1160,39 @@ namespace Microsoft.CodeAnalysis.Testing diagnostics.AddRange(additionalDiagnostics); var results = SortDistinctDiagnostics(diagnostics); return results; + + static async Task> GetCompilerDiagnosticsAsync(AnalyzerTest self, Compilation compilation, ImmutableArray analyzers, AnalyzerOptions analyzerOptions, CancellationToken cancellationToken) + { + if (!analyzers.Any(static analyzer => IsCompilerDiagnosticSuppressor(analyzer))) + { + return compilation.GetDiagnostics(cancellationToken); + } + + // Need to get the compiler diagnostics through a new CompilationWithAnalyzers instance to ensure + // suppressions are applied. + var compilerSuppressors = analyzers.Where(static analyzer => IsCompilerDiagnosticSuppressor(analyzer)).ToImmutableArray(); + var compilationWithAnalyzers = self.CreateCompilationWithAnalyzers(compilation, compilerSuppressors, analyzerOptions, cancellationToken); + return await compilationWithAnalyzers.GetAllDiagnosticsAsync().ConfigureAwait(false); + } + + static bool IsCompilerDiagnosticSuppressor(DiagnosticAnalyzer analyzer) + { + if (!DiagnosticSuppressorWrapper.IsInstance(analyzer)) + { + return false; + } + + var wrapper = DiagnosticSuppressorWrapper.FromInstance(analyzer); + foreach (var descriptor in wrapper.SupportedSuppressions) + { + if (IsCompilerDiagnosticId(descriptor.SuppressedDiagnosticId)) + { + return true; + } + } + + return false; + } } private protected static bool IsNonLocalDiagnostic(Diagnostic diagnostic) diff --git a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Lightup/DiagnosticSuppressorWrapper.cs b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Lightup/DiagnosticSuppressorWrapper.cs new file mode 100644 index 00000000..dfef90b9 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Lightup/DiagnosticSuppressorWrapper.cs @@ -0,0 +1,71 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.CodeAnalysis.Testing.Lightup +{ + internal readonly struct DiagnosticSuppressorWrapper + { + internal const string WrappedTypeName = "Microsoft.CodeAnalysis.Diagnostics.DiagnosticSuppressor"; + internal static readonly Type? WrappedType = typeof(Diagnostic).GetTypeInfo().Assembly.GetType(WrappedTypeName); + private static readonly Func s_supportedSuppressions; + + private readonly DiagnosticAnalyzer _instance; + + static DiagnosticSuppressorWrapper() + { + s_supportedSuppressions = LightupHelpers.CreatePropertyAccessor(WrappedType, nameof(SupportedSuppressions), Enumerable.Empty()); + } + + private DiagnosticSuppressorWrapper(DiagnosticAnalyzer instance) + { + _instance = instance; + } + + public ImmutableArray SupportedSuppressions + { + get + { + var suppressions = s_supportedSuppressions(_instance).Cast(); + return ImmutableArray.CreateRange(suppressions.Select(SuppressionDescriptorWrapper.FromInstance)); + } + } + + public static DiagnosticSuppressorWrapper FromInstance(DiagnosticAnalyzer instance) + { + if (instance == null) + { + return default; + } + + if (!IsInstance(instance)) + { + throw new InvalidCastException($"Cannot cast '{instance.GetType().FullName}' to '{WrappedTypeName}'"); + } + + return new DiagnosticSuppressorWrapper(instance); + } + + public static bool IsInstance(object value) + { + if (value is null) + { + return false; + } + + if (WrappedType is null) + { + return false; + } + + return WrappedType.IsAssignableFrom(value.GetType()); + } + } +} diff --git a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Lightup/LightupHelpers.cs b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Lightup/LightupHelpers.cs index 59f3c5fc..09110c54 100644 --- a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Lightup/LightupHelpers.cs +++ b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Lightup/LightupHelpers.cs @@ -61,7 +61,7 @@ namespace Microsoft.CodeAnalysis.Testing.Lightup Expression> expression = Expression.Lambda>( - Expression.Call(instance, property.GetMethod), + Expression.Convert(Expression.Call(instance, property.GetMethod), typeof(TResult)), parameter); return expression.Compile(); } diff --git a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Lightup/SuppressionDescriptorWrapper.cs b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Lightup/SuppressionDescriptorWrapper.cs new file mode 100644 index 00000000..b38ed144 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Lightup/SuppressionDescriptorWrapper.cs @@ -0,0 +1,68 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Reflection; + +namespace Microsoft.CodeAnalysis.Testing.Lightup +{ + internal readonly struct SuppressionDescriptorWrapper + { + internal const string WrappedTypeName = "Microsoft.CodeAnalysis.SuppressionDescriptor"; + internal static readonly Type? WrappedType = typeof(Diagnostic).GetTypeInfo().Assembly.GetType(WrappedTypeName); + private static readonly Func s_id; + private static readonly Func s_suppressedDiagnosticId; + private static readonly Func s_justification; + + private readonly object _instance; + + static SuppressionDescriptorWrapper() + { + s_id = LightupHelpers.CreatePropertyAccessor(WrappedType, nameof(Id), string.Empty); + s_suppressedDiagnosticId = LightupHelpers.CreatePropertyAccessor(WrappedType, nameof(SuppressedDiagnosticId), string.Empty); + s_justification = LightupHelpers.CreatePropertyAccessor(WrappedType, nameof(Justification), string.Empty); + } + + private SuppressionDescriptorWrapper(object instance) + { + _instance = instance; + } + + public string Id => s_id(_instance); + + public string SuppressedDiagnosticId => s_suppressedDiagnosticId(_instance); + + public LocalizableString Justification => s_justification(_instance); + + public static SuppressionDescriptorWrapper FromInstance(object instance) + { + if (instance == null) + { + return default; + } + + if (!IsInstance(instance)) + { + throw new InvalidCastException($"Cannot cast '{instance.GetType().FullName}' to '{WrappedTypeName}'"); + } + + return new SuppressionDescriptorWrapper(instance); + } + + public static bool IsInstance(object value) + { + if (value is null) + { + return false; + } + + if (WrappedType is null) + { + return false; + } + + return WrappedType.IsAssignableFrom(value.GetType()); + } + } +} diff --git a/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.UnitTests/DiagnosticSuppressorTests.cs b/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.UnitTests/DiagnosticSuppressorTests.cs index abd676f5..5cfd364a 100644 --- a/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.UnitTests/DiagnosticSuppressorTests.cs +++ b/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.UnitTests/DiagnosticSuppressorTests.cs @@ -12,6 +12,9 @@ using Microsoft.CodeAnalysis.Testing.TestAnalyzers; using Xunit; using CSharpAnalyzerTest = Microsoft.CodeAnalysis.Testing.TestAnalyzers.CSharpAnalyzerTest< Microsoft.CodeAnalysis.Testing.TestAnalyzers.HighlightBracesAnalyzer>; +using CSharpCompilerTest = Microsoft.CodeAnalysis.Testing.TestAnalyzers.CSharpSuppressorTest< + Microsoft.CodeAnalysis.Testing.EmptyDiagnosticAnalyzer, + Microsoft.CodeAnalysis.Testing.DiagnosticSuppressorTests.NonNullableFieldSuppressor>; using CSharpTest = Microsoft.CodeAnalysis.Testing.TestAnalyzers.CSharpSuppressorTest< Microsoft.CodeAnalysis.Testing.TestAnalyzers.HighlightBracesAnalyzer, Microsoft.CodeAnalysis.Testing.DiagnosticSuppressorTests.HighlightBracesSuppressor>; @@ -54,6 +57,29 @@ namespace Microsoft.CodeAnalysis.Testing }.RunAsync(); } + [Fact] + [WorkItem(1090, "https://github.com/dotnet/roslyn-sdk/issues/1090")] + public async Task TestSuppressionOfCompilerDiagnostic() + { + await new CSharpCompilerTest + { + CompilerDiagnostics = CompilerDiagnostics.Warnings, + TestState = + { + Sources = + { + @"#nullable enable +class Sample { string {|#0:_value|}; }", + }, + ExpectedDiagnostics = + { + DiagnosticResult.CompilerWarning("CS8618").WithLocation(0).WithIsSuppressed(true), + DiagnosticResult.CompilerWarning("CS0169").WithLocation(0), + }, + }, + }.RunAsync(); + } + [Fact] public async Task TestUnexpectedSuppressionPresent() { @@ -134,6 +160,23 @@ namespace Microsoft.CodeAnalysis.Testing } } } + + [DiagnosticAnalyzer(LanguageNames.CSharp)] + internal class NonNullableFieldSuppressor : DiagnosticSuppressor + { + internal static readonly SuppressionDescriptor Descriptor = + new SuppressionDescriptor("FieldIsAssigned", "CS8618", "justification"); + + public override ImmutableArray SupportedSuppressions => ImmutableArray.Create(Descriptor); + + public override void ReportSuppressions(SuppressionAnalysisContext context) + { + foreach (var diagnostic in context.ReportedDiagnostics) + { + context.ReportSuppression(Suppression.Create(Descriptor, diagnostic)); + } + } + } } }