|
// 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);
}
}
|