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 e4464ec2..ee80d77c 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 @@ -1157,7 +1157,8 @@ namespace Microsoft.CodeAnalysis.Testing } diagnostics.AddRange(additionalDiagnostics); - var results = SortDistinctDiagnostics(diagnostics); + var filteredDiagnostics = FilterDiagnostics(diagnostics.ToImmutable()); + var results = SortDistinctDiagnostics(filteredDiagnostics); return results; static async Task> GetCompilerDiagnosticsAsync(AnalyzerTest self, Compilation compilation, ImmutableArray analyzers, AnalyzerOptions analyzerOptions, CancellationToken cancellationToken) @@ -1677,13 +1678,28 @@ namespace Microsoft.CodeAnalysis.Testing protected abstract ParseOptions CreateParseOptions(); + /// + /// Filter s to only include items of interest to testing. By default, this includes all + /// unsuppressed diagnostics, and all diagnostics suppressed by a + /// . + /// + /// A collection of s to be filtered. + /// A collection containing the input , filtered to only include + /// diagnostics relevant for testing. + protected virtual ImmutableArray<(Project project, Diagnostic diagnostic)> FilterDiagnostics(ImmutableArray<(Project project, Diagnostic diagnostic)> diagnostics) + { + return diagnostics + .Where(d => !d.diagnostic.IsSuppressed() || d.diagnostic.ProgrammaticSuppressionInfo() != null) + .ToImmutableArray(); + } + /// /// Sort s by location in source document. /// /// A collection of s to be sorted. /// A collection containing the input , sorted by /// and . - protected virtual ImmutableArray<(Project project, Diagnostic diagnostic)> SortDistinctDiagnostics(IEnumerable<(Project project, Diagnostic diagnostic)> diagnostics) + protected virtual ImmutableArray<(Project project, Diagnostic diagnostic)> SortDistinctDiagnostics(ImmutableArray<(Project project, Diagnostic diagnostic)> diagnostics) { return diagnostics .OrderBy(d => d.diagnostic.Location.GetLineSpan().Path, StringComparer.Ordinal) 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 a2a3e942..bb80805d 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 @@ -22,10 +22,19 @@ namespace Microsoft.CodeAnalysis.Testing.Extensions nameof(IsSuppressed), defaultValue: false); + private static readonly Func s_grogrammaticSuppressionInfo = + LightupHelpers.CreatePropertyAccessor( + typeof(Diagnostic), + nameof(ProgrammaticSuppressionInfo), + defaultValue: null); + public static IReadOnlyList Arguments(this Diagnostic diagnostic) => s_arguments(diagnostic); public static bool IsSuppressed(this Diagnostic diagnostic) => s_isSuppressed(diagnostic); + + public static ProgrammaticSuppressionInfoWrapper? ProgrammaticSuppressionInfo(this Diagnostic diagnostic) + => s_grogrammaticSuppressionInfo(diagnostic) is { } info ? ProgrammaticSuppressionInfoWrapper.FromInstance(info) : null; } } diff --git a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Lightup/ProgrammaticSuppressionInfoWrapper.cs b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Lightup/ProgrammaticSuppressionInfoWrapper.cs new file mode 100644 index 00000000..297830bb --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Lightup/ProgrammaticSuppressionInfoWrapper.cs @@ -0,0 +1,61 @@ +// 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.Immutable; +using System.Reflection; + +namespace Microsoft.CodeAnalysis.Testing.Lightup +{ + internal readonly struct ProgrammaticSuppressionInfoWrapper + { + internal const string WrappedTypeName = "Microsoft.CodeAnalysis.Diagnostics.ProgrammaticSuppressionInfo"; + internal static readonly Type? WrappedType = typeof(Diagnostic).GetTypeInfo().Assembly.GetType(WrappedTypeName); + private static readonly Func> s_suppressions; + + private readonly object _instance; + + static ProgrammaticSuppressionInfoWrapper() + { + s_suppressions = LightupHelpers.CreatePropertyAccessor>(WrappedType, nameof(Suppressions), ImmutableHashSet<(string id, LocalizableString justification)>.Empty); + } + + private ProgrammaticSuppressionInfoWrapper(object instance) + { + _instance = instance; + } + + public ImmutableHashSet<(string id, LocalizableString justification)> Suppressions => s_suppressions(_instance); + + public static ProgrammaticSuppressionInfoWrapper FromInstance(object instance) + { + if (instance == null) + { + return default; + } + + if (!IsInstance(instance)) + { + throw new InvalidCastException($"Cannot cast '{instance.GetType().FullName}' to '{WrappedTypeName}'"); + } + + return new ProgrammaticSuppressionInfoWrapper(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/PublicAPI.Unshipped.txt b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/PublicAPI.Unshipped.txt index 0720b398..ed9fd1c3 100644 --- a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/PublicAPI.Unshipped.txt +++ b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/PublicAPI.Unshipped.txt @@ -350,13 +350,14 @@ virtual Microsoft.CodeAnalysis.Testing.AnalyzerTest.CreateWorkspaceIm virtual Microsoft.CodeAnalysis.Testing.AnalyzerTest.DefaultFilePath.get -> string virtual Microsoft.CodeAnalysis.Testing.AnalyzerTest.DefaultFilePathPrefix.get -> string virtual Microsoft.CodeAnalysis.Testing.AnalyzerTest.DefaultTestProjectName.get -> string +virtual Microsoft.CodeAnalysis.Testing.AnalyzerTest.FilterDiagnostics(System.Collections.Immutable.ImmutableArray<(Microsoft.CodeAnalysis.Project project, Microsoft.CodeAnalysis.Diagnostic diagnostic)> diagnostics) -> System.Collections.Immutable.ImmutableArray<(Microsoft.CodeAnalysis.Project project, Microsoft.CodeAnalysis.Diagnostic diagnostic)> virtual Microsoft.CodeAnalysis.Testing.AnalyzerTest.GetAnalyzerOptions(Microsoft.CodeAnalysis.Project project) -> Microsoft.CodeAnalysis.Diagnostics.AnalyzerOptions virtual Microsoft.CodeAnalysis.Testing.AnalyzerTest.GetDefaultDiagnostic(Microsoft.CodeAnalysis.Diagnostics.DiagnosticAnalyzer[] analyzers) -> Microsoft.CodeAnalysis.DiagnosticDescriptor virtual Microsoft.CodeAnalysis.Testing.AnalyzerTest.GetProjectCompilationAsync(Microsoft.CodeAnalysis.Project project, Microsoft.CodeAnalysis.Testing.IVerifier verifier, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<(Microsoft.CodeAnalysis.Compilation compilation, System.Collections.Immutable.ImmutableArray generatorDiagnostics)> virtual Microsoft.CodeAnalysis.Testing.AnalyzerTest.GetSourceGenerators() -> System.Collections.Generic.IEnumerable virtual Microsoft.CodeAnalysis.Testing.AnalyzerTest.IsCompilerDiagnosticIncluded(Microsoft.CodeAnalysis.Diagnostic diagnostic, Microsoft.CodeAnalysis.Testing.CompilerDiagnostics compilerDiagnostics) -> bool virtual Microsoft.CodeAnalysis.Testing.AnalyzerTest.RunImplAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task -virtual Microsoft.CodeAnalysis.Testing.AnalyzerTest.SortDistinctDiagnostics(System.Collections.Generic.IEnumerable<(Microsoft.CodeAnalysis.Project project, Microsoft.CodeAnalysis.Diagnostic diagnostic)> diagnostics) -> System.Collections.Immutable.ImmutableArray<(Microsoft.CodeAnalysis.Project project, Microsoft.CodeAnalysis.Diagnostic diagnostic)> +virtual Microsoft.CodeAnalysis.Testing.AnalyzerTest.SortDistinctDiagnostics(System.Collections.Immutable.ImmutableArray<(Microsoft.CodeAnalysis.Project project, Microsoft.CodeAnalysis.Diagnostic diagnostic)> diagnostics) -> System.Collections.Immutable.ImmutableArray<(Microsoft.CodeAnalysis.Project project, Microsoft.CodeAnalysis.Diagnostic diagnostic)> virtual Microsoft.CodeAnalysis.Testing.CodeActionTest.FilterCodeActions(System.Collections.Immutable.ImmutableArray actions) -> System.Collections.Immutable.ImmutableArray virtual Microsoft.CodeAnalysis.Testing.DefaultVerifier.CreateMessage(string message) -> string virtual Microsoft.CodeAnalysis.Testing.DefaultVerifier.Empty(string collectionName, System.Collections.Generic.IEnumerable collection) -> void diff --git a/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.UnitTests/AutoExclusionTests.cs b/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.UnitTests/AutoExclusionTests.cs index 570eef6f..596ad6a6 100644 --- a/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.UnitTests/AutoExclusionTests.cs +++ b/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.UnitTests/AutoExclusionTests.cs @@ -220,6 +220,54 @@ End Class }.RunAsync(); } + [Fact] + public async Task TestCSharpTestIgnoresPragmaSuppressedWarnings() + { + var testCode = @"class TestClass {|#0:{|} }"; + + // A diagnostic is produced in the original code + await new CSharpAnalyzerTest + { + TestCode = testCode, + ExpectedDiagnostics = + { + new DiagnosticResult("Brace", DiagnosticSeverity.Warning).WithLocation(0), + }, + }.RunAsync(); + + // The same diagnostic is ignored when suppressed using '#pragma warning disable' + await new CSharpAnalyzerTest + { + TestCode = $@"#pragma warning disable Brace +{testCode}", + }.RunAsync(); + } + + [Fact] + public async Task TestVisualBasicTestIgnoresPragmaSuppressedWarnings() + { + var testCode = @"Class TestClass + Dim Field As Integer() = {|#0:{|} } +End Class"; + + // A diagnostic is produced in the original code + await new VisualBasicAnalyzerTest + { + TestCode = testCode, + ExpectedDiagnostics = + { + new DiagnosticResult("Brace", DiagnosticSeverity.Warning).WithLocation(0), + }, + }.RunAsync(); + + // The same diagnostic is ignored when suppressed using '#Disable Warning' + await new VisualBasicAnalyzerTest + { + TestCode = $@"#Disable Warning Brace +{testCode}", + }.RunAsync(); + } + [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] private class ReplaceThisWithBaseAnalyzer : DiagnosticAnalyzer { @@ -314,6 +362,15 @@ End Class } } + [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] + private class HighlightBraceSpanAnalyzer : AbstractHighlightBracesAnalyzer + { + protected override Diagnostic CreateDiagnostic(SyntaxToken token) + { + return Diagnostic.Create(Descriptor, token.GetLocation()); + } + } + private class CSharpReplaceThisWithBaseTest : CSharpAnalyzerTest { private readonly GeneratedCodeAnalysisFlags? _generatedCodeAnalysisFlags;