File: src\Analyzers\CSharp\Tests\RemoveUnnecessarySuppressions\RemoveUnnecessaryAttributeSuppressionsTests.cs
Web Access
Project: src\src\CodeStyle\CSharp\Tests\Microsoft.CodeAnalysis.CSharp.CodeStyle.UnitTests.csproj (Microsoft.CodeAnalysis.CSharp.CodeStyle.UnitTests)
// 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.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.RemoveUnnecessarySuppressions;
using Microsoft.CodeAnalysis.Editor.UnitTests.CodeActions;
using Microsoft.CodeAnalysis.RemoveUnnecessarySuppressions;
using Microsoft.CodeAnalysis.Test.Utilities;
using Roslyn.Test.Utilities;
using Xunit;
 
namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.RemoveUnnecessarySuppressions;
 
using VerifyCS = CSharpCodeFixVerifier<
    CSharpRemoveUnnecessaryAttributeSuppressionsDiagnosticAnalyzer,
    RemoveUnnecessaryAttributeSuppressionsCodeFixProvider>;
 
[Trait(Traits.Feature, Traits.Features.CodeActionsRemoveUnnecessarySuppressions)]
[WorkItem("https://github.com/dotnet/roslyn/issues/44176")]
public sealed class RemoveUnnecessaryAttributeSuppressionsTests
{
    [Theory, CombinatorialData]
    public void TestStandardProperty(AnalyzerProperty property)
        => VerifyCS.VerifyStandardProperty(property);
 
    [Theory]
    // Field
    [InlineData("""
        Scope = "member"
        """, """
        Target = "~F:N.C.F"
        """, "assembly")]
    // Property
    [InlineData("""
        Scope = "member"
        """, """
        Target = "~P:N.C.P"
        """, "assembly")]
    // Method
    [InlineData("""
        Scope = "member"
        """, """
        Target = "~M:N.C.M()"
        """, "assembly")]
    // Type
    [InlineData("""
        Scope = "member"
        """, """
        Target = "~T:N.C"
        """, "assembly")]
    // Namespace
    [InlineData("""
        Scope = "namespace"
        """, """
        Target = "~N:N"
        """, "assembly")]
    // NamespaceAndDescendants
    [InlineData("""
        Scope = "namespaceanddescendants"
        """, """
        Target = "~N:N"
        """, "assembly")]
    // Module - no scope, no target
    [InlineData(null, null, "assembly")]
    // Module - no target
    [InlineData("""
        Scope = "module"
        """, null, "assembly")]
    // Module - null target
    [InlineData("""
        Scope = "module"
        """, @"Target = null", "assembly")]
    // Resource - not handled
    [InlineData("""
        Scope = "resource"
        """, """
        Target = ""
        """, "assembly")]
    // 'module' attribute target
    [InlineData("""
        Scope = "member"
        """, """
        Target = "~M:N.C.M()"
        """, "module")]
    // Member with non-matching scope (seems to be respected by suppression decoder)
    [InlineData("""
        Scope = "type"
        """, """
        Target = "~M:N.C.M()"
        """, "assembly")]
    [InlineData("""
        Scope = "namespace"
        """, """
        Target = "~F:N.C.F"
        """, "assembly")]
    // Case insensitive scope
    [InlineData("""
        Scope = "Member"
        """, """
        Target = "~F:N.C.F"
        """, "assembly")]
    [InlineData("""
        Scope = "MEMBER"
        """, """
        Target = "~F:N.C.F"
        """, "assembly")]
    public async Task ValidSuppressions(string? scope, string? target, string attributeTarget)
    {
        var scopeString = scope != null ? $@", {scope}" : string.Empty;
        var targetString = target != null ? $@", {target}" : string.Empty;
 
        var input = $$"""

            [{{attributeTarget}}: System.Diagnostics.CodeAnalysis.SuppressMessage("Category", "Id: Title", Justification = "Pending"{{scopeString}}{{targetString}})]
 
            namespace N
            {
                class C
                {
                    public int F;
                    public int P { get; }
                    public void M() { }
                }
            }
            """;
        await VerifyCS.VerifyCodeFixAsync(input, input);
    }
 
    [Theory]
    // Field - no matching symbol
    [InlineData("""
        Scope = "member"
        """, """
        Target = "~F:N.C.F2"
        """, "assembly")]
    // Field - no matching symbol (case insensitive)
    [InlineData("""
        Scope = "Member"
        """, """
        Target = "~F:N.C.F2"
        """, "assembly")]
    [InlineData("""
        Scope = "MEMBER"
        """, """
        Target = "~F:N.C.F2"
        """, "assembly")]
    // Property - invalid scope
    [InlineData("""
        Scope = "invalid"
        """, """
        Target = "~P:N.C.P"
        """, "assembly")]
    // Method - wrong signature
    [InlineData("""
        Scope = "member"
        """, """
        Target = "~M:N.C.M(System.Int32)"
        """, "assembly")]
    // Method - module scope
    [InlineData("""
        Scope = "module"
        """, """
        Target = "~M:N.C.M()"
        """, "assembly")]
    // Method - null scope
    [InlineData(@"Scope = null", """
        Target = "~M:N.C.M()"
        """, "assembly")]
    // Method - no scope
    [InlineData(null, """
        Target = "~M:N.C.M()"
        """, "assembly")]
    // Member scope - null target
    [InlineData("""
        Scope = "member"
        """, @"Target = null", "assembly")]
    // Member scope - no target
    [InlineData("""
        Scope = "member"
        """, null, "assembly")]
    // Type - no matching namespace
    [InlineData("""
        Scope = "type"
        """, """
        Target = "~T:N2.C"
        """, "assembly")]
    // Namespace - extra namespace qualification
    [InlineData("""
        Scope = "namespace"
        """, """
        Target = "~N:N.N2"
        """, "assembly")]
    // NamespaceAndDescendants - empty target
    [InlineData("""
        Scope = "namespaceanddescendants"
        """, """
        Target = ""
        """, "assembly")]
    // Module - no scope, empty target
    [InlineData(null, """
        Target = ""
        """, "assembly")]
    // Module - no scope, non-empty target
    [InlineData(null, """
        Target = "~T:N.C"
        """, "assembly")]
    // Module scope, empty target
    [InlineData("""
        Scope = "module"
        """, """
        Target = ""
        """, "assembly")]
    // Module no scope, non-empty target
    [InlineData("""
        Scope = "module"
        """, """
        Target = "~T:N.C"
        """, "assembly")]
    public async Task InvalidSuppressions(string? scope, string? target, string attributeTarget)
    {
        var scopeString = scope != null ? $@", {scope}" : string.Empty;
        var targetString = target != null ? $@", {target}" : string.Empty;
        await VerifyCS.VerifyCodeFixAsync($$"""

            [{{attributeTarget}}: [|System.Diagnostics.CodeAnalysis.SuppressMessage("Category", "Id: Title", Justification = "Pending"{{scopeString}}{{targetString}})|]]
 
            namespace N
            {
                class C
                {
                    public int F;
                    public int P { get; }
                    public void M() { }
                }
            }
            """, $$"""

 
            namespace N
            {
                class C
                {
                    public int F;
                    public int P { get; }
                    public void M() { }
                }
            }
            """);
    }
 
    [Fact]
    public async Task ValidAndInvalidSuppressions()
    {
        var attributePrefix = """
            System.Diagnostics.CodeAnalysis.SuppressMessage("Category", "Id: Title", Justification = "Pending"
            """;
        var validSuppression = $@"{attributePrefix}, Scope = ""member"", Target = ""~T:C"")";
        var invalidSuppression = $@"[|{attributePrefix}, Scope = ""member"", Target = """")|]";
        await VerifyCS.VerifyCodeFixAsync($$"""

            [assembly: {{validSuppression}}]
            [assembly: {{invalidSuppression}}]
            [assembly: {{validSuppression}}, {{validSuppression}}]
            [assembly: {{invalidSuppression}}, {{invalidSuppression}}]
            [assembly: {{validSuppression}}, {{invalidSuppression}}]
            [assembly: {{invalidSuppression}}, {{validSuppression}}]
            [assembly: {{invalidSuppression}}, {{validSuppression}}, {{invalidSuppression}}, {{validSuppression}}]
 
            class C { }

            """, $$"""

            [assembly: {{validSuppression}}]
            [assembly: {{validSuppression}}, {{validSuppression}}]
            [assembly: {{validSuppression}}]
            [assembly: {{validSuppression}}]
            [assembly: {{validSuppression}}, {{validSuppression}}]
 
            class C { }

            """);
    }
 
    [Theory]
    [InlineData("")]
    [InlineData("""
        , Scope = "member", Target = "~M:C.M()"
        """)]
    [InlineData("""
        , Scope = "invalid", Target = "invalid"
        """)]
    public async Task LocalSuppressions(string scopeAndTarget)
    {
        var input = $$"""

            [System.Diagnostics.CodeAnalysis.SuppressMessage("Category", "Id: Title", Justification = "Pending"{{scopeAndTarget}})]
            class C
            {
                public void M() { }
            }
            """;
        await VerifyCS.VerifyCodeFixAsync(input, input);
    }
 
    [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/45465")]
    public async Task LegacyModeGlobalSuppressionWithNamespaceAndDescendantsScope()
    {
        var target = "N:N.InvalidChild";
        var input = $$"""

            [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Category", "Id: Title", Scope = "namespaceanddescendants", Target = {|#0:"{{target}}"|})]
 
            namespace N
            {
                class C { }
            }
            """;
        var expectedDiagnostic = VerifyCS.Diagnostic(AbstractRemoveUnnecessaryAttributeSuppressionsDiagnosticAnalyzer.LegacyFormatTargetDescriptor)
                                    .WithLocation(0)
                                    .WithArguments(target);
 
        await VerifyCS.VerifyCodeFixAsync(input, expectedDiagnostic, input);
    }
}