diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/SwaggerActionReturnType.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/SwaggerActionReturnType.cs index f202f43c1b4..a700f142e20 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Rules/SwaggerActionReturnType.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Rules/SwaggerActionReturnType.cs @@ -32,6 +32,9 @@ public sealed class SwaggerActionReturnType : SonarDiagnosticAnalyzer private static readonly ImmutableArray ControllerActionReturnTypes = ImmutableArray.Create( KnownType.Microsoft_AspNetCore_Mvc_IActionResult, KnownType.Microsoft_AspNetCore_Http_IResult); + private static readonly ImmutableArray ProducesAttributes = ImmutableArray.Create( + KnownType.Microsoft_AspNetCore_Mvc_ProducesAttribute, + KnownType.Microsoft_AspNetCore_Mvc_ProducesResponseTypeAttribute); private static HashSet ActionResultMethods => [ "Ok", @@ -85,7 +88,7 @@ private static InvalidMethodResult InvalidMethod(BaseMethodDeclarationSyntax met || !method.ReturnType.DerivesOrImplementsAny(ControllerActionReturnTypes) || method.GetAttributesWithInherited().Any(x => x.AttributeClass.DerivesFrom(KnownType.Microsoft_AspNetCore_Mvc_ApiConventionMethodAttribute) || HasApiExplorerSettingsWithIgnoreApiTrue(x) - || HasProducesResponseTypeAttributeWithReturnType(x)) + || HasProducesAttributesWithReturnType(x)) ? null : new InvalidMethodResult(method, responseInvocations); } @@ -124,7 +127,7 @@ private static bool IsControllerCandidate(ISymbol symbol) foreach (var attribute in symbol.GetAttributesWithInherited()) { if (attribute.AttributeClass.DerivesFrom(KnownType.Microsoft_AspNetCore_Mvc_ApiConventionTypeAttribute) - || HasProducesResponseTypeAttributeWithReturnType(attribute) + || HasProducesAttributesWithReturnType(attribute) || HasApiExplorerSettingsWithIgnoreApiTrue(attribute)) { return false; @@ -139,10 +142,10 @@ private static bool IsControllerCandidate(ISymbol symbol) ? NoTypeMessageFormat : NoAttributeMessageFormat; - private static bool HasProducesResponseTypeAttributeWithReturnType(AttributeData attribute) => + private static bool HasProducesAttributesWithReturnType(AttributeData attribute) => attribute.AttributeClass.DerivesFrom(KnownType.Microsoft_AspNetCore_Mvc_ProducesResponseTypeAttribute_T) - || (attribute.AttributeClass.DerivesFrom(KnownType.Microsoft_AspNetCore_Mvc_ProducesResponseTypeAttribute) - && ContainsReturnType(attribute)); + || attribute.AttributeClass.DerivesFrom(KnownType.Microsoft_AspNetCore_Mvc_ProducesAttribute_T) + || (attribute.AttributeClass.DerivesFromAny(ProducesAttributes) && ContainsReturnType(attribute)); private static bool HasApiExplorerSettingsWithIgnoreApiTrue(AttributeData attribute) => attribute.AttributeClass.DerivesFrom(KnownType.Microsoft_AspNetCore_Mvc_ApiExplorerSettingsAttribute) diff --git a/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs b/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs index b63c7eec774..22010e04f12 100644 --- a/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs +++ b/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs @@ -92,6 +92,8 @@ public sealed partial class KnownType public static readonly KnownType Microsoft_AspNetCore_Mvc_NonActionAttribute = new("Microsoft.AspNetCore.Mvc.NonActionAttribute"); public static readonly KnownType Microsoft_AspNetCore_Mvc_NonControllerAttribute = new("Microsoft.AspNetCore.Mvc.NonControllerAttribute"); public static readonly KnownType Microsoft_AspNetCore_Mvc_ObjectResult = new("Microsoft.AspNetCore.Mvc.ObjectResult"); + public static readonly KnownType Microsoft_AspNetCore_Mvc_ProducesAttribute = new("Microsoft.AspNetCore.Mvc.ProducesAttribute"); + public static readonly KnownType Microsoft_AspNetCore_Mvc_ProducesAttribute_T = new("Microsoft.AspNetCore.Mvc.ProducesAttribute", "T"); public static readonly KnownType Microsoft_AspNetCore_Mvc_ProducesResponseTypeAttribute = new("Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute"); public static readonly KnownType Microsoft_AspNetCore_Mvc_ProducesResponseTypeAttribute_T = new("Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute", "T"); public static readonly KnownType Microsoft_AspNetCore_Mvc_RazorPages_PageModel = new("Microsoft.AspNetCore.Mvc.RazorPages.PageModel"); diff --git a/analyzers/tests/SonarAnalyzer.Test/TestCases/SwaggerActionReturnType.cs b/analyzers/tests/SonarAnalyzer.Test/TestCases/SwaggerActionReturnType.cs index 8b88b8dad79..56dd1eacf69 100644 --- a/analyzers/tests/SonarAnalyzer.Test/TestCases/SwaggerActionReturnType.cs +++ b/analyzers/tests/SonarAnalyzer.Test/TestCases/SwaggerActionReturnType.cs @@ -15,6 +15,14 @@ public class CompliantBaseline : Controller [HttpGet("foo")] public IActionResult NotSuccessfulResult() => BadRequest(foo); + [HttpGet("foo")] + [Produces(typeof(Foo))] + public IActionResult HasProducesTypeOf() => Ok(foo); + + [HttpGet("foo")] + [Produces()] + public IActionResult HasProducesGeneric() => Ok(foo); + [HttpGet("foo")] [ProducesResponseType(typeof(Foo), StatusCodes.Status200OK)] public IActionResult HasProducesResponseTypeTypeOf() => Ok(foo); @@ -151,6 +159,11 @@ public class NocompliantBaseline : ControllerBase // ^^^^^^^ } + [HttpGet("foo")] + [Produces("text/plain")] + public IActionResult HasProducesTypeOf() => // Noncompliant + Ok(foo); // Secondary + [Route("foo")] [ProducesResponseType(StatusCodes.Status200OK)] public IActionResult AnnotatedWithNoType() // Noncompliant {{Use the ProducesResponseType overload containing the return type for successful responses.}}