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 3557ccf3..04573d0c 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 @@ -412,15 +412,8 @@ namespace Microsoft.CodeAnalysis.Testing if (expected.IsSuppressed.HasValue) { - if (actual.diagnostic.TryGetIsSuppressed(out var actualValue)) - { - message = FormatVerifierMessage(analyzers, actual.diagnostic, expected, $"Expected diagnostic suppression state to match"); - verifier.Equal(expected.IsSuppressed.Value, actualValue, message); - } - else - { - throw new NotSupportedException("DiagnosticSuppressors are not supported on this platform."); - } + message = FormatVerifierMessage(analyzers, actual.diagnostic, expected, $"Expected diagnostic suppression state to match"); + verifier.Equal(expected.IsSuppressed.Value, actual.diagnostic.IsSuppressed(), message); } DiagnosticVerifier?.Invoke(actual.diagnostic, expected, verifier); @@ -817,11 +810,9 @@ namespace Microsoft.CodeAnalysis.Testing builder.Append(")"); } - if (diagnostics[i].TryGetIsSuppressed(out var isSuppressed) && isSuppressed) + if (diagnostics[i].IsSuppressed()) { - builder.Append($".{nameof(DiagnosticResult.WithIsSuppressed)}("); - builder.Append(isSuppressed); - builder.Append(")"); + builder.Append($".{nameof(DiagnosticResult.WithIsSuppressed)}(true)"); } builder.AppendLine(","); @@ -910,7 +901,7 @@ namespace Microsoft.CodeAnalysis.Testing if (diagnostics[i].IsSuppressed.HasValue) { builder.Append($".{nameof(DiagnosticResult.WithIsSuppressed)}("); - builder.Append(diagnostics[i].IsSuppressed.GetValueOrDefault()); + builder.Append(diagnostics[i].IsSuppressed.GetValueOrDefault() ? "true" : "false"); builder.Append(")"); } diff --git a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Extensions/DiagnosticExtensions.cs b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Extensions/DiagnosticExtensions.cs index 4c4a2afa..3bd98793 100644 --- a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Extensions/DiagnosticExtensions.cs +++ b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Extensions/DiagnosticExtensions.cs @@ -3,30 +3,28 @@ // See the LICENSE file in the project root for more information. using System; -using System.Collections.Generic; using System.Reflection; -using System.Text; namespace Microsoft.CodeAnalysis.Testing.Extensions { internal static class DiagnosticExtensions { - private static readonly MethodInfo? IsSuppressedGetMethod = typeof(Diagnostic).GetProperty("IsSuppressed")?.GetMethod; + private static readonly Func s_isSuppressed; - public static bool TryGetIsSuppressed(this Diagnostic diagnostic, out bool value) + static DiagnosticExtensions() { - value = false; - - var rawValue = IsSuppressedGetMethod?.Invoke(diagnostic, null); - - if (rawValue?.GetType() != typeof(bool)) + var isSuppressedProperty = typeof(Diagnostic).GetProperty(nameof(IsSuppressed), typeof(bool)); + if (isSuppressedProperty is { GetMethod: { } getMethod }) { - return false; + s_isSuppressed = (Func)getMethod.CreateDelegate(typeof(Func), target: null); + } + else + { + s_isSuppressed = diagnostic => false; } - - value = (bool)rawValue; - - return true; } + + public static bool IsSuppressed(this Diagnostic diagnostic) + => s_isSuppressed(diagnostic); } } 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 new file mode 100644 index 00000000..abd676f5 --- /dev/null +++ b/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.UnitTests/DiagnosticSuppressorTests.cs @@ -0,0 +1,140 @@ +// 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. + +#if !NETCOREAPP1_1 && !NET46 + +using System; +using System.Collections.Immutable; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing.TestAnalyzers; +using Xunit; +using CSharpAnalyzerTest = Microsoft.CodeAnalysis.Testing.TestAnalyzers.CSharpAnalyzerTest< + Microsoft.CodeAnalysis.Testing.TestAnalyzers.HighlightBracesAnalyzer>; +using CSharpTest = Microsoft.CodeAnalysis.Testing.TestAnalyzers.CSharpSuppressorTest< + Microsoft.CodeAnalysis.Testing.TestAnalyzers.HighlightBracesAnalyzer, + Microsoft.CodeAnalysis.Testing.DiagnosticSuppressorTests.HighlightBracesSuppressor>; + +namespace Microsoft.CodeAnalysis.Testing +{ + public class DiagnosticSuppressorTests + { + private static readonly DiagnosticDescriptor DiagnosticDescriptor = new HighlightBracesAnalyzer().Descriptor; + + [Fact] + public async Task TestUnspecifiedSuppression() + { + await new CSharpTest + { + TestState = + { + Sources = { "namespace MyNamespace {|#0:{|} }" }, + ExpectedDiagnostics = + { + new DiagnosticResult(DiagnosticDescriptor).WithLocation(0).WithIsSuppressed(null), + }, + }, + }.RunAsync(); + } + + [Fact] + public async Task TestNormalSuppression() + { + await new CSharpTest + { + TestState = + { + Sources = { "namespace MyNamespace {|#0:{|} }" }, + ExpectedDiagnostics = + { + new DiagnosticResult(DiagnosticDescriptor).WithLocation(0).WithIsSuppressed(true), + }, + }, + }.RunAsync(); + } + + [Fact] + public async Task TestUnexpectedSuppressionPresent() + { + var exception = await Assert.ThrowsAsync(async () => + { + await new CSharpTest + { + TestState = + { + Sources = { "namespace MyNamespace {|#0:{|} }" }, + ExpectedDiagnostics = + { + new DiagnosticResult(DiagnosticDescriptor).WithLocation(0).WithIsSuppressed(false), + }, + }, + }.RunAsync(); + }); + + var expected = + "Expected diagnostic suppression state to match" + Environment.NewLine + + Environment.NewLine + + "Expected diagnostic:" + Environment.NewLine + + " // /0/Test0.cs(1,23,1,24): warning Brace: message" + Environment.NewLine + + "VerifyCS.Diagnostic().WithSpan(1, 23, 1, 24).WithIsSuppressed(false)," + Environment.NewLine + + Environment.NewLine + + "Actual diagnostic:" + Environment.NewLine + + " // /0/Test0.cs(1,23): warning Brace: message" + Environment.NewLine + + "VerifyCS.Diagnostic().WithSpan(1, 23, 1, 24).WithIsSuppressed(true)," + Environment.NewLine + + Environment.NewLine; + new DefaultVerifier().EqualOrDiff(expected, exception.Message); + } + + [Fact] + public async Task TestExpectedSuppressionMissing() + { + var exception = await Assert.ThrowsAsync(async () => + { + await new CSharpAnalyzerTest + { + TestState = + { + Sources = { "namespace MyNamespace {|#0:{|} }" }, + ExpectedDiagnostics = + { + new DiagnosticResult(DiagnosticDescriptor).WithLocation(0).WithIsSuppressed(true), + }, + }, + }.RunAsync(); + }); + + var expected = + "Expected diagnostic suppression state to match" + Environment.NewLine + + Environment.NewLine + + "Expected diagnostic:" + Environment.NewLine + + " // /0/Test0.cs(1,23,1,24): warning Brace: message" + Environment.NewLine + + "VerifyCS.Diagnostic().WithSpan(1, 23, 1, 24).WithIsSuppressed(true)," + Environment.NewLine + + Environment.NewLine + + "Actual diagnostic:" + Environment.NewLine + + " // /0/Test0.cs(1,23): warning Brace: message" + Environment.NewLine + + "VerifyCS.Diagnostic().WithSpan(1, 23, 1, 24)," + Environment.NewLine + + Environment.NewLine; + new DefaultVerifier().EqualOrDiff(expected, exception.Message); + } + + [DiagnosticAnalyzer(LanguageNames.CSharp)] + internal class HighlightBracesSuppressor : DiagnosticSuppressor + { + internal static readonly SuppressionDescriptor Descriptor = + new SuppressionDescriptor("XBrace", DiagnosticDescriptor.Id, "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)); + } + } + } + } +} + +#endif diff --git a/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.UnitTests/TestAnalyzers/CSharpSuppressorTest`2.cs b/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.UnitTests/TestAnalyzers/CSharpSuppressorTest`2.cs new file mode 100644 index 00000000..70b47ef7 --- /dev/null +++ b/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.UnitTests/TestAnalyzers/CSharpSuppressorTest`2.cs @@ -0,0 +1,22 @@ +// 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. + +#if !NETCOREAPP1_1 && !NET46 + +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.CodeAnalysis.Testing.TestAnalyzers +{ + internal class CSharpSuppressorTest : CSharpAnalyzerTest + where TAnalyzer : DiagnosticAnalyzer, new() + where TSuppressor : DiagnosticSuppressor, new() + { + protected override IEnumerable GetDiagnosticAnalyzers() + => base.GetDiagnosticAnalyzers().Concat(new[] { new TSuppressor() }); + } +} + +#endif