From c83dc727d56a59311778de0971f5f4bbb276c23c Mon Sep 17 00:00:00 2001
From: Costin Zaharia
<56015273+costin-zaharia-sonarsource@users.noreply.github.com>
Date: Wed, 24 Apr 2024 17:55:43 +0200
Subject: [PATCH] New rule S6968 for C#: Actions that return a value should be
annotated with ProducesResponseTypeAttribute containing the return type
(#9185)
---
README.md | 2 +-
.../AspNetCore8/AspNetCore8.csproj | 4 +
.../Controllers/S6968Controller.cs | 12 +
.../AspNetCore8/Models/Foo.cs | 6 +
.../S1451-AspNetCore8-net8.0.json | 12 +
.../S6934-AspNetCore8-net8.0.json | 10 +
.../S6968-AspNetCore8-net8.0.json | 10 +
analyzers/rspec/cs/S6968.html | 98 ++++++
analyzers/rspec/cs/S6968.json | 23 ++
analyzers/rspec/cs/Sonar_way_profile.json | 3 +-
.../Rules/SwaggerActionReturnType.cs | 156 ++++++++++
.../SonarReportingContextBase.cs | 3 +
.../Extensions/CompilationExtensions.cs | 3 +
.../Helpers/KnownAssembly.cs | 2 +
.../SonarAnalyzer.Common/Helpers/KnownType.cs | 10 +-
.../PackagingTests/RuleTypeMappingCS.cs | 2 +-
.../Rules/SwaggerActionReturnTypeTest.cs | 168 +++++++++++
.../TestCases/SwaggerActionReturnType.cs | 280 ++++++++++++++++++
.../AspNetCoreMetadataReference.cs | 1 +
.../NuGetMetadataReference.cs | 2 +
20 files changed, 803 insertions(+), 4 deletions(-)
create mode 100644 analyzers/its/Projects/ManuallyAddedNoncompliantIssues.CS/AspNetCore8/Controllers/S6968Controller.cs
create mode 100644 analyzers/its/Projects/ManuallyAddedNoncompliantIssues.CS/AspNetCore8/Models/Foo.cs
create mode 100644 analyzers/its/expected/ManuallyAddedNoncompliantIssues.CS/S6934-AspNetCore8-net8.0.json
create mode 100644 analyzers/its/expected/ManuallyAddedNoncompliantIssues.CS/S6968-AspNetCore8-net8.0.json
create mode 100644 analyzers/rspec/cs/S6968.html
create mode 100644 analyzers/rspec/cs/S6968.json
create mode 100644 analyzers/src/SonarAnalyzer.CSharp/Rules/SwaggerActionReturnType.cs
create mode 100644 analyzers/tests/SonarAnalyzer.Test/Rules/SwaggerActionReturnTypeTest.cs
create mode 100644 analyzers/tests/SonarAnalyzer.Test/TestCases/SwaggerActionReturnType.cs
diff --git a/README.md b/README.md
index c6fc2fd0054..cca91e516fe 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@ languages in [SonarQube](https://www.sonarsource.com/products/sonarqube), [Sonar
## Features
-* [450+ C# rules](https://rules.sonarsource.com/csharp) and [210+ VB.NET rules](https://rules.sonarsource.com/vbnet)
+* [460+ C# rules](https://rules.sonarsource.com/csharp) and [210+ VB.NET rules](https://rules.sonarsource.com/vbnet)
* Metrics (cognitive complexity, duplications, number of lines, etc.)
* Import of [test coverage reports](https://community.sonarsource.com/t/9871) from Visual Studio Code Coverage, dotCover, OpenCover, Coverlet, Altcover.
* Import of third-party Roslyn Analyzers results
diff --git a/analyzers/its/Projects/ManuallyAddedNoncompliantIssues.CS/AspNetCore8/AspNetCore8.csproj b/analyzers/its/Projects/ManuallyAddedNoncompliantIssues.CS/AspNetCore8/AspNetCore8.csproj
index 1b28a01c81c..3f6f1da0e28 100644
--- a/analyzers/its/Projects/ManuallyAddedNoncompliantIssues.CS/AspNetCore8/AspNetCore8.csproj
+++ b/analyzers/its/Projects/ManuallyAddedNoncompliantIssues.CS/AspNetCore8/AspNetCore8.csproj
@@ -6,4 +6,8 @@
enable
+
+
+
+
diff --git a/analyzers/its/Projects/ManuallyAddedNoncompliantIssues.CS/AspNetCore8/Controllers/S6968Controller.cs b/analyzers/its/Projects/ManuallyAddedNoncompliantIssues.CS/AspNetCore8/Controllers/S6968Controller.cs
new file mode 100644
index 00000000000..d5bc3eb0a49
--- /dev/null
+++ b/analyzers/its/Projects/ManuallyAddedNoncompliantIssues.CS/AspNetCore8/Controllers/S6968Controller.cs
@@ -0,0 +1,12 @@
+using AspNetCore8.Models;
+using Microsoft.AspNetCore.Mvc;
+
+namespace AspNetCore8.Controllers;
+
+[ApiController]
+public class S6968Controller : ControllerBase
+{
+ [HttpGet("foo")]
+ public IActionResult ReturnsOkWithValue() // Noncompliant
+ => Ok(new Foo()); // Secondary
+}
diff --git a/analyzers/its/Projects/ManuallyAddedNoncompliantIssues.CS/AspNetCore8/Models/Foo.cs b/analyzers/its/Projects/ManuallyAddedNoncompliantIssues.CS/AspNetCore8/Models/Foo.cs
new file mode 100644
index 00000000000..112cade1998
--- /dev/null
+++ b/analyzers/its/Projects/ManuallyAddedNoncompliantIssues.CS/AspNetCore8/Models/Foo.cs
@@ -0,0 +1,6 @@
+namespace AspNetCore8.Models;
+
+public class Foo
+{
+ public int Bar { get; set; }
+}
diff --git a/analyzers/its/expected/ManuallyAddedNoncompliantIssues.CS/S1451-AspNetCore8-net8.0.json b/analyzers/its/expected/ManuallyAddedNoncompliantIssues.CS/S1451-AspNetCore8-net8.0.json
index 41c9ac1ee81..822691d1478 100644
--- a/analyzers/its/expected/ManuallyAddedNoncompliantIssues.CS/S1451-AspNetCore8-net8.0.json
+++ b/analyzers/its/expected/ManuallyAddedNoncompliantIssues.CS/S1451-AspNetCore8-net8.0.json
@@ -12,12 +12,24 @@
"Uri": "https://github.com/SonarSource/sonar-dotnet/blob/master/analyzers/its/Projects/ManuallyAddedNoncompliantIssues.CS/AspNetCore8/Controllers/S6930Controller.cs#L1",
"Location": "Line 1 Position 1-1"
},
+ {
+ "Id": "S1451",
+ "Message": "Add or update the header of this file.",
+ "Uri": "https://github.com/SonarSource/sonar-dotnet/blob/master/analyzers/its/Projects/ManuallyAddedNoncompliantIssues.CS/AspNetCore8/Controllers/S6968Controller.cs#L1",
+ "Location": "Line 1 Position 1-1"
+ },
{
"Id": "S1451",
"Message": "Add or update the header of this file.",
"Uri": "https://github.com/SonarSource/sonar-dotnet/blob/master/analyzers/its/Projects/ManuallyAddedNoncompliantIssues.CS/AspNetCore8/Models/ErrorViewModel.cs#L1",
"Location": "Line 1 Position 1-1"
},
+ {
+ "Id": "S1451",
+ "Message": "Add or update the header of this file.",
+ "Uri": "https://github.com/SonarSource/sonar-dotnet/blob/master/analyzers/its/Projects/ManuallyAddedNoncompliantIssues.CS/AspNetCore8/Models/Foo.cs#L1",
+ "Location": "Line 1 Position 1-1"
+ },
{
"Id": "S1451",
"Message": "Add or update the header of this file.",
diff --git a/analyzers/its/expected/ManuallyAddedNoncompliantIssues.CS/S6934-AspNetCore8-net8.0.json b/analyzers/its/expected/ManuallyAddedNoncompliantIssues.CS/S6934-AspNetCore8-net8.0.json
new file mode 100644
index 00000000000..03c43553d4f
--- /dev/null
+++ b/analyzers/its/expected/ManuallyAddedNoncompliantIssues.CS/S6934-AspNetCore8-net8.0.json
@@ -0,0 +1,10 @@
+{
+ "Issues": [
+ {
+ "Id": "S6934",
+ "Message": "Specify the RouteAttribute when an HttpMethodAttribute or RouteAttribute is specified at an action level.",
+ "Uri": "https://github.com/SonarSource/sonar-dotnet/blob/master/analyzers/its/Projects/ManuallyAddedNoncompliantIssues.CS/AspNetCore8/Controllers/S6968Controller.cs#L7",
+ "Location": "Line 7 Position 14-29"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/analyzers/its/expected/ManuallyAddedNoncompliantIssues.CS/S6968-AspNetCore8-net8.0.json b/analyzers/its/expected/ManuallyAddedNoncompliantIssues.CS/S6968-AspNetCore8-net8.0.json
new file mode 100644
index 00000000000..c5827819d5e
--- /dev/null
+++ b/analyzers/its/expected/ManuallyAddedNoncompliantIssues.CS/S6968-AspNetCore8-net8.0.json
@@ -0,0 +1,10 @@
+{
+ "Issues": [
+ {
+ "Id": "S6968",
+ "Message": "Annotate this method with ProducesResponseType containing the return type for successful responses.",
+ "Uri": "https://github.com/SonarSource/sonar-dotnet/blob/master/analyzers/its/Projects/ManuallyAddedNoncompliantIssues.CS/AspNetCore8/Controllers/S6968Controller.cs#L10",
+ "Location": "Line 10 Position 26-44"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/analyzers/rspec/cs/S6968.html b/analyzers/rspec/cs/S6968.html
new file mode 100644
index 00000000000..ea9c057196d
--- /dev/null
+++ b/analyzers/rspec/cs/S6968.html
@@ -0,0 +1,98 @@
+
In an ASP.NET Core Web API,
+controller actions can optionally return a result value. If a controller action returns a value in the happy path, for example ControllerBase.Ok(Object),
+annotating the action with one of the [ProducesResponseType]
+overloads that describe the type is recommended.
+Why is this an issue?
+If an ASP.NET Core Web API uses Swagger, the API documentation will be generated based on the input/output types
+of the controller actions, as well as the attributes annotating the actions. If an action returns IActionResult or IResult, Swagger cannot infer the type of the response. From
+the consumer’s perspective, this can be confusing and lead to unexpected results and bugs in the long run without the API provider’s awareness.
+This rule raises an issue on a controller action when:
+
+ - The action returns a value in the happy path. This can be either:
+
+ - There is no
[ProducesResponseType]
attribute containing the return type, either at controller or action level.
+ - There is no
[SwaggerResponse]
attribute containing the return type, either at controller or action level.
+ - The controller is annotated with the
[ApiController]
attribute.
+
+ - The controller action returns either IActionResult or IResult.
+ - The application has enabled the Swagger
+ middleware.
+
+How to fix it
+There are multiple ways to fix this issue:
+
+Code examples
+Noncompliant code example
+
+[HttpGet("foo")]
+// Noncompliant: Annotate this method with ProducesResponseType containing the return type for succesful responses.
+public IActionResult MagicNumber() => Ok(42);
+
+
+[HttpGet("foo")]
+// Noncompliant: Use the ProducesResponseType overload containing the return type for succesful responses.
+[ProducesResponseType(StatusCodes.Status200OK)]
+public IActionResult MagicNumber() => Ok(42);
+
+Compliant solution
+
+[HttpGet("foo")]
+[ProducesResponseType<int>(StatusCodes.Status200OK)]
+public IActionResult MagicNumber() => Ok(42);
+
+
+[HttpGet("foo")]
+[ProducesResponseType(typeof(int), StatusCodes.Status200OK)]
+public IActionResult MagicNumber() => Ok(42);
+
+Resources
+Documentation
+
+
diff --git a/analyzers/rspec/cs/S6968.json b/analyzers/rspec/cs/S6968.json
new file mode 100644
index 00000000000..b71ae41b9ca
--- /dev/null
+++ b/analyzers/rspec/cs/S6968.json
@@ -0,0 +1,23 @@
+{
+ "title": "Actions that return a value should be annotated with ProducesResponseTypeAttribute containing the return type",
+ "type": "CODE_SMELL",
+ "status": "ready",
+ "remediation": {
+ "func": "Constant\/Issue",
+ "constantCost": "5min"
+ },
+ "tags": [
+ "asp.net"
+ ],
+ "defaultSeverity": "Major",
+ "ruleSpecification": "RSPEC-6968",
+ "sqKey": "S6968",
+ "scope": "Main",
+ "quickfix": "partial",
+ "code": {
+ "impacts": {
+ "MAINTAINABILITY": "HIGH"
+ },
+ "attribute": "CLEAR"
+ }
+}
diff --git a/analyzers/rspec/cs/Sonar_way_profile.json b/analyzers/rspec/cs/Sonar_way_profile.json
index e3006805225..e0756762ecd 100644
--- a/analyzers/rspec/cs/Sonar_way_profile.json
+++ b/analyzers/rspec/cs/Sonar_way_profile.json
@@ -319,6 +319,7 @@
"S6961",
"S6962",
"S6965",
- "S6966"
+ "S6966",
+ "S6968"
]
}
diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/SwaggerActionReturnType.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/SwaggerActionReturnType.cs
new file mode 100644
index 00000000000..f202f43c1b4
--- /dev/null
+++ b/analyzers/src/SonarAnalyzer.CSharp/Rules/SwaggerActionReturnType.cs
@@ -0,0 +1,156 @@
+/*
+ * SonarAnalyzer for .NET
+ * Copyright (C) 2015-2024 SonarSource SA
+ * mailto: contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+namespace SonarAnalyzer.Rules.CSharp;
+
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public sealed class SwaggerActionReturnType : SonarDiagnosticAnalyzer
+{
+ private const string DiagnosticId = "S6968";
+ private const string MessageFormat = "{0}";
+ private const string NoAttributeMessageFormat = "Annotate this method with ProducesResponseType containing the return type for successful responses.";
+ private const string NoTypeMessageFormat = "Use the ProducesResponseType overload containing the return type for successful responses.";
+
+ private static readonly DiagnosticDescriptor Rule = DescriptorFactory.Create(DiagnosticId, MessageFormat);
+ private static readonly ImmutableArray ControllerActionReturnTypes = ImmutableArray.Create(
+ KnownType.Microsoft_AspNetCore_Mvc_IActionResult,
+ KnownType.Microsoft_AspNetCore_Http_IResult);
+ private static HashSet ActionResultMethods =>
+ [
+ "Ok",
+ "Created",
+ "CreatedAtAction",
+ "CreatedAtRoute",
+ "Accepted",
+ "AcceptedAtAction",
+ "AcceptedAtRoute"
+ ];
+ private static HashSet ResultMethods =>
+ [
+ "Ok",
+ "Created",
+ "CreatedAtRoute",
+ "Accepted",
+ "AcceptedAtRoute"
+ ];
+
+ public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule);
+
+ protected override void Initialize(SonarAnalysisContext context) =>
+ context.RegisterCompilationStartAction(compilationStart =>
+ {
+ if (!compilationStart.Compilation.Assembly.HasAttribute(KnownType.Microsoft_AspNetCore_Mvc_ApiConventionTypeAttribute)
+ && compilationStart.Compilation.ReferencesAll(KnownAssembly.MicrosoftAspNetCoreMvcCore, KnownAssembly.SwashbuckleAspNetCoreSwagger))
+ {
+ compilationStart.RegisterSymbolStartAction(symbolStart =>
+ {
+ if (IsControllerCandidate(symbolStart.Symbol))
+ {
+ symbolStart.RegisterSyntaxNodeAction(nodeContext =>
+ {
+ var methodDeclaration = (MethodDeclarationSyntax)nodeContext.Node;
+ if (InvalidMethod(methodDeclaration, nodeContext) is { } method)
+ {
+ nodeContext.ReportIssue(Rule, methodDeclaration.Identifier, additionalLocations: method.ResponseInvocations, GetMessage(method.Symbol));
+ }
+ }, SyntaxKind.MethodDeclaration);
+ }
+ }, SymbolKind.NamedType);
+ }
+ });
+
+ private static InvalidMethodResult InvalidMethod(BaseMethodDeclarationSyntax methodDeclaration, SonarSyntaxNodeReportingContext nodeContext)
+ {
+ var responseInvocations = FindSuccessResponses(methodDeclaration, nodeContext.SemanticModel);
+ return responseInvocations.Length == 0
+ || nodeContext.SemanticModel.GetDeclaredSymbol(methodDeclaration, nodeContext.Cancel) is not { } method
+ || !method.IsControllerMethod()
+ || !method.ReturnType.DerivesOrImplementsAny(ControllerActionReturnTypes)
+ || method.GetAttributesWithInherited().Any(x => x.AttributeClass.DerivesFrom(KnownType.Microsoft_AspNetCore_Mvc_ApiConventionMethodAttribute)
+ || HasApiExplorerSettingsWithIgnoreApiTrue(x)
+ || HasProducesResponseTypeAttributeWithReturnType(x))
+ ? null
+ : new InvalidMethodResult(method, responseInvocations);
+ }
+
+ private static SyntaxNode[] FindSuccessResponses(SyntaxNode node, SemanticModel model)
+ {
+ return ActionResultInvocations().Concat(ObjectCreationInvocations()).Concat(ResultMethodsInvocations()).ToArray();
+
+ IEnumerable ActionResultInvocations() =>
+ node.DescendantNodes()
+ .OfType()
+ .Where(x => ActionResultMethods.Contains(x.GetName())
+ && x.ArgumentList.Arguments.Count > 0
+ && model.GetSymbolInfo(x.Expression).Symbol is { } symbol
+ && symbol.IsInType(KnownType.Microsoft_AspNetCore_Mvc_ControllerBase)
+ && symbol.GetParameters().Any(parameter => parameter.HasAttribute(KnownType.Microsoft_AspNetCore_Mvc_Infrastructure_ActionResultObjectValueAttribute)));
+
+ IEnumerable ObjectCreationInvocations() =>
+ node.DescendantNodes()
+ .OfType()
+ .Where(x => x.GetName() == "ObjectResult"
+ && x.ArgumentList?.Arguments.Count > 0
+ && model.GetSymbolInfo(x.Type).Symbol.GetSymbolType().Is(KnownType.Microsoft_AspNetCore_Mvc_ObjectResult));
+
+ IEnumerable ResultMethodsInvocations() =>
+ node.DescendantNodes()
+ .OfType()
+ .Where(x => ResultMethods.Contains(x.GetName())
+ && x.ArgumentList.Arguments.Count > 0
+ && model.GetSymbolInfo(x).Symbol.IsInType(KnownType.Microsoft_AspNetCore_Http_Results));
+ }
+
+ private static bool IsControllerCandidate(ISymbol symbol)
+ {
+ var hasApiControllerAttribute = false;
+ foreach (var attribute in symbol.GetAttributesWithInherited())
+ {
+ if (attribute.AttributeClass.DerivesFrom(KnownType.Microsoft_AspNetCore_Mvc_ApiConventionTypeAttribute)
+ || HasProducesResponseTypeAttributeWithReturnType(attribute)
+ || HasApiExplorerSettingsWithIgnoreApiTrue(attribute))
+ {
+ return false;
+ }
+ hasApiControllerAttribute = hasApiControllerAttribute || attribute.AttributeClass.DerivesFrom(KnownType.Microsoft_AspNetCore_Mvc_ApiControllerAttribute);
+ }
+ return hasApiControllerAttribute;
+ }
+
+ private static string GetMessage(ISymbol symbol) =>
+ symbol.GetAttributesWithInherited().Any(x => x.AttributeClass.DerivesFrom(KnownType.Microsoft_AspNetCore_Mvc_ProducesResponseTypeAttribute))
+ ? NoTypeMessageFormat
+ : NoAttributeMessageFormat;
+
+ private static bool HasProducesResponseTypeAttributeWithReturnType(AttributeData attribute) =>
+ attribute.AttributeClass.DerivesFrom(KnownType.Microsoft_AspNetCore_Mvc_ProducesResponseTypeAttribute_T)
+ || (attribute.AttributeClass.DerivesFrom(KnownType.Microsoft_AspNetCore_Mvc_ProducesResponseTypeAttribute)
+ && ContainsReturnType(attribute));
+
+ private static bool HasApiExplorerSettingsWithIgnoreApiTrue(AttributeData attribute) =>
+ attribute.AttributeClass.DerivesFrom(KnownType.Microsoft_AspNetCore_Mvc_ApiExplorerSettingsAttribute)
+ && attribute.NamedArguments.FirstOrDefault(x => x.Key == "IgnoreApi").Value.Value is true;
+
+ private static bool ContainsReturnType(AttributeData attribute) =>
+ !attribute.ConstructorArguments.FirstOrDefault(x => x.Type.Is(KnownType.System_Type)).IsNull
+ || attribute.NamedArguments.FirstOrDefault(x => x.Key == "Type").Value.Value is not null;
+
+ private sealed record InvalidMethodResult(IMethodSymbol Symbol, SyntaxNode[] ResponseInvocations);
+}
diff --git a/analyzers/src/SonarAnalyzer.Common/AnalysisContext/SonarReportingContextBase.cs b/analyzers/src/SonarAnalyzer.Common/AnalysisContext/SonarReportingContextBase.cs
index b98b193513d..aa592ac0c67 100644
--- a/analyzers/src/SonarAnalyzer.Common/AnalysisContext/SonarReportingContextBase.cs
+++ b/analyzers/src/SonarAnalyzer.Common/AnalysisContext/SonarReportingContextBase.cs
@@ -100,6 +100,9 @@ public abstract class SonarTreeReportingContextBase : SonarReportingCo
public void ReportIssue(DiagnosticDescriptor rule, SyntaxToken locationToken, params object[] messageArgs) =>
ReportIssue(rule, locationToken.GetLocation(), messageArgs);
+ public void ReportIssue(DiagnosticDescriptor rule, SyntaxToken locationToken, IEnumerable additionalLocations, params object[] messageArgs) =>
+ ReportIssueCore(Diagnostic.Create(rule, locationToken.GetLocation(), additionalLocations.Select(x => x.GetLocation()), messageArgs));
+
public void ReportIssue(DiagnosticDescriptor rule, Location location, params object[] messageArgs) =>
ReportIssueCore(Diagnostic.Create(rule, location, messageArgs));
}
diff --git a/analyzers/src/SonarAnalyzer.Common/Extensions/CompilationExtensions.cs b/analyzers/src/SonarAnalyzer.Common/Extensions/CompilationExtensions.cs
index ab9b45f7e6b..d65a3a0fa22 100644
--- a/analyzers/src/SonarAnalyzer.Common/Extensions/CompilationExtensions.cs
+++ b/analyzers/src/SonarAnalyzer.Common/Extensions/CompilationExtensions.cs
@@ -38,6 +38,9 @@ public static bool ReferencesAny(this Compilation compilation, params KnownAssem
? Array.Exists(assemblies, x => compilation.References(x))
: throw new ArgumentException("Assemblies argument needs to be non-empty");
+ public static bool ReferencesAll(this Compilation compilation, params KnownAssembly[] assemblies)
+ => Array.TrueForAll(assemblies, x => compilation.References(x));
+
public static bool References(this Compilation compilation, KnownAssembly assembly)
=> assembly.IsReferencedBy(compilation);
diff --git a/analyzers/src/SonarAnalyzer.Common/Helpers/KnownAssembly.cs b/analyzers/src/SonarAnalyzer.Common/Helpers/KnownAssembly.cs
index 9418f926b74..e0bae650a7f 100644
--- a/analyzers/src/SonarAnalyzer.Common/Helpers/KnownAssembly.cs
+++ b/analyzers/src/SonarAnalyzer.Common/Helpers/KnownAssembly.cs
@@ -45,6 +45,8 @@ public sealed partial class KnownAssembly
// Logging assemblies
public static KnownAssembly MicrosoftExtensionsLoggingAbstractions { get; } = new(NameAndPublicKeyIs("Microsoft.Extensions.Logging.Abstractions", "adb9793829ddae60"));
public static KnownAssembly Serilog { get; } = new(NameAndPublicKeyIs("Serilog", "24c2f752a8e58a10"));
+ public static KnownAssembly MicrosoftAspNetCoreMvcCore { get; } = new(NameAndPublicKeyIs("Microsoft.AspNetCore.Mvc.Core", "adb9793829ddae60"));
+ public static KnownAssembly SwashbuckleAspNetCoreSwagger { get; } = new(NameAndPublicKeyIs("Swashbuckle.AspNetCore.Swagger", "62657d7474907593"));
public static KnownAssembly NLog { get; } = new(NameAndPublicKeyIs("NLog", "5120e14c03d0593c"));
public static KnownAssembly Log4Net { get; } = new(NameIs("log4net").And(PublicKeyTokenIsAny("669e0ddf0bb1aa2a", "1b44e1d426115821")));
public static KnownAssembly CommonLoggingCore { get; } = new(NameAndPublicKeyIs("Common.Logging.Core", "af08829b84f0328e"));
diff --git a/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs b/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs
index 7a6d51772b4..b63c7eec774 100644
--- a/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs
+++ b/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs
@@ -67,8 +67,13 @@ public sealed partial class KnownType
public static readonly KnownType Microsoft_AspNetCore_Http_IHeaderDictionary = new("Microsoft.AspNetCore.Http.IHeaderDictionary");
public static readonly KnownType Microsoft_AspNetCore_Http_IRequestCookieCollection = new("Microsoft.AspNetCore.Http.IRequestCookieCollection");
public static readonly KnownType Microsoft_AspNetCore_Http_IResponseCookies = new("Microsoft.AspNetCore.Http.IResponseCookies");
+ public static readonly KnownType Microsoft_AspNetCore_Http_IResult = new("Microsoft.AspNetCore.Http.IResult");
+ public static readonly KnownType Microsoft_AspNetCore_Http_Results = new("Microsoft.AspNetCore.Http.Results");
public static readonly KnownType Microsoft_AspNetCore_Mvc_AcceptVerbsAttribute = new("Microsoft.AspNetCore.Mvc.AcceptVerbsAttribute");
public static readonly KnownType Microsoft_AspNetCore_Mvc_ApiControllerAttribute = new("Microsoft.AspNetCore.Mvc.ApiControllerAttribute");
+ public static readonly KnownType Microsoft_AspNetCore_Mvc_ApiConventionMethodAttribute = new("Microsoft.AspNetCore.Mvc.ApiConventionMethodAttribute");
+ public static readonly KnownType Microsoft_AspNetCore_Mvc_ApiConventionTypeAttribute = new("Microsoft.AspNetCore.Mvc.ApiConventionTypeAttribute");
+ public static readonly KnownType Microsoft_AspNetCore_Mvc_ApiExplorerSettingsAttribute = new("Microsoft.AspNetCore.Mvc.ApiExplorerSettingsAttribute");
public static readonly KnownType Microsoft_AspNetCore_Mvc_Controller = new("Microsoft.AspNetCore.Mvc.Controller");
public static readonly KnownType Microsoft_AspNetCore_Mvc_ControllerBase = new("Microsoft.AspNetCore.Mvc.ControllerBase");
public static readonly KnownType Microsoft_AspNetCore_Mvc_ControllerAttribute = new("Microsoft.AspNetCore.Mvc.ControllerAttribute");
@@ -76,7 +81,6 @@ public sealed partial class KnownType
public static readonly KnownType Microsoft_AspNetCore_Mvc_FromServicesAttribute = new("Microsoft.AspNetCore.Mvc.FromServicesAttribute");
public static readonly KnownType Microsoft_AspNetCore_Mvc_HttpDeleteAttribute = new("Microsoft.AspNetCore.Mvc.HttpDeleteAttribute");
public static readonly KnownType Microsoft_AspNetCore_Mvc_HttpGetAttribute = new("Microsoft.AspNetCore.Mvc.HttpGetAttribute");
- public static readonly KnownType Microsoft_AspNetCore_Mvc_ApiExplorerSettingsAttribute = new("Microsoft.AspNetCore.Mvc.ApiExplorerSettingsAttribute");
public static readonly KnownType Microsoft_AspNetCore_Mvc_HttpHeadAttribute = new("Microsoft.AspNetCore.Mvc.HttpHeadAttribute");
public static readonly KnownType Microsoft_AspNetCore_Mvc_HttpOptionsAttribute = new("Microsoft.AspNetCore.Mvc.HttpOptionsAttribute");
public static readonly KnownType Microsoft_AspNetCore_Mvc_HttpPatchAttribute = new("Microsoft.AspNetCore.Mvc.HttpPatchAttribute");
@@ -84,8 +88,12 @@ public sealed partial class KnownType
public static readonly KnownType Microsoft_AspNetCore_Mvc_HttpPutAttribute = new("Microsoft.AspNetCore.Mvc.HttpPutAttribute");
public static readonly KnownType Microsoft_AspNetCore_Mvc_IActionResult = new("Microsoft.AspNetCore.Mvc.IActionResult");
public static readonly KnownType Microsoft_AspNetCore_Mvc_IgnoreAntiforgeryTokenAttribute = new("Microsoft.AspNetCore.Mvc.IgnoreAntiforgeryTokenAttribute");
+ public static readonly KnownType Microsoft_AspNetCore_Mvc_Infrastructure_ActionResultObjectValueAttribute = new("Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute");
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_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");
public static readonly KnownType Microsoft_AspNetCore_Mvc_RequestFormLimitsAttribute = new("Microsoft.AspNetCore.Mvc.RequestFormLimitsAttribute");
public static readonly KnownType Microsoft_AspNetCore_Mvc_RequestSizeLimitAttribute = new("Microsoft.AspNetCore.Mvc.RequestSizeLimitAttribute");
diff --git a/analyzers/tests/SonarAnalyzer.Test/PackagingTests/RuleTypeMappingCS.cs b/analyzers/tests/SonarAnalyzer.Test/PackagingTests/RuleTypeMappingCS.cs
index c7b14227677..cc5fe8c2ad7 100644
--- a/analyzers/tests/SonarAnalyzer.Test/PackagingTests/RuleTypeMappingCS.cs
+++ b/analyzers/tests/SonarAnalyzer.Test/PackagingTests/RuleTypeMappingCS.cs
@@ -6892,7 +6892,7 @@ internal static class RuleTypeMappingCS
["S6965"] = "CODE_SMELL",
["S6966"] = "CODE_SMELL",
// ["S6967"],
- // ["S6968"],
+ ["S6968"] = "CODE_SMELL",
// ["S6969"],
// ["S6970"],
// ["S6971"],
diff --git a/analyzers/tests/SonarAnalyzer.Test/Rules/SwaggerActionReturnTypeTest.cs b/analyzers/tests/SonarAnalyzer.Test/Rules/SwaggerActionReturnTypeTest.cs
new file mode 100644
index 00000000000..1f732923eb4
--- /dev/null
+++ b/analyzers/tests/SonarAnalyzer.Test/Rules/SwaggerActionReturnTypeTest.cs
@@ -0,0 +1,168 @@
+/*
+ * SonarAnalyzer for .NET
+ * Copyright (C) 2015-2024 SonarSource SA
+ * mailto: contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#if NET
+
+using SonarAnalyzer.Rules.CSharp;
+
+namespace SonarAnalyzer.Test.Rules;
+
+[TestClass]
+public class SwaggerActionReturnTypeTest
+{
+ private readonly VerifierBuilder builder = new VerifierBuilder()
+ .WithOptions(ParseOptionsHelper.FromCSharp11)
+ .AddReferences([
+ ..NuGetMetadataReference.SwashbuckleAspNetCoreAnnotations(),
+ ..NuGetMetadataReference.SwashbuckleAspNetCoreSwagger(),
+ AspNetCoreMetadataReference.MicrosoftAspNetCoreHttpAbstractions,
+ AspNetCoreMetadataReference.MicrosoftAspNetCoreHttpResults,
+ AspNetCoreMetadataReference.MicrosoftAspNetCoreMvcCore,
+ AspNetCoreMetadataReference.MicrosoftAspNetCoreMvcViewFeatures,
+ AspNetCoreMetadataReference.MicrosoftAspNetCoreMvcAbstractions,
+ ]);
+
+ [TestMethod]
+ public void SwaggerActionReturnType_CS() =>
+ builder.AddPaths("SwaggerActionReturnType.cs").Verify();
+
+ [DataTestMethod]
+ [DataRow("""Ok(bar)""")]
+ [DataRow("""Created("uri", bar)""")]
+ [DataRow("""Created(new Uri("uri"), bar)""")]
+ [DataRow("""CreatedAtAction("actionName", bar)""")]
+ [DataRow("""CreatedAtAction("actionName", null, bar)""")]
+ [DataRow("""CreatedAtAction("actionName", "controllerName", null, bar)""")]
+ [DataRow("""CreatedAtRoute("routeName", bar)""")]
+ [DataRow("""CreatedAtRoute("default(object)", bar)""")]
+ [DataRow("""CreatedAtRoute("routeName", null, bar)""")]
+ [DataRow("""Accepted(bar)""")]
+ [DataRow("""Accepted("uri", bar)""")]
+ [DataRow("""Accepted(new Uri("uri"), bar)""")]
+ [DataRow("""AcceptedAtAction("actionName", bar)""")]
+ [DataRow("""AcceptedAtAction("actionName", "controllerName", bar)""")]
+ [DataRow("""AcceptedAtAction("actionName", "controllerName", null, bar)""")]
+ [DataRow("""AcceptedAtRoute("routeName", null, bar)""")]
+ [DataRow("""AcceptedAtRoute(default(object), bar)""")]
+ public void SwaggerActionReturnType_IActionResult(string invocation) =>
+ builder
+ .AddSnippet($$"""
+ using System;
+ using Microsoft.AspNetCore.Mvc;
+
+ [ApiController]
+ public class Foo : Controller
+ {
+ [HttpGet("a")]
+ public IActionResult Method() => // Noncompliant
+ {{invocation}}; // Secondary
+
+ private Bar bar = new();
+ }
+
+ public class Bar {}
+ """)
+ .Verify();
+
+ [DataTestMethod]
+ [DataRow("""Accepted("uri")""")]
+ [DataRow("""Accepted(uri)""")]
+ public void SwaggerActionReturnType_IActionResult_Compliant(string invocation) =>
+ builder
+ .AddSnippet($$"""
+ using System;
+ using Microsoft.AspNetCore.Mvc;
+
+ [ApiController]
+ public class Foo : Controller
+ {
+ [HttpGet("a")]
+ public IActionResult Method() =>
+ {{invocation}};
+
+ private Bar bar = new();
+ private Uri uri;
+ }
+
+ public class Bar {}
+ """)
+ .Verify();
+
+ [DataTestMethod]
+ [DataRow("""Results.Ok(bar)""")]
+ [DataRow("""Results.Ok((object) bar)""")]
+ [DataRow("""Results.Ok(bar)""")]
+ [DataRow("""Results.Created("uri", bar)""")]
+ [DataRow("""Results.Created("uri", (object) bar)""")]
+ [DataRow("""Results.Created(new Uri("uri"), bar)""")]
+ [DataRow("""Results.Created(new Uri("uri"), (object) bar)""")]
+ [DataRow("""Results.CreatedAtRoute(value: (object) bar)""")]
+ [DataRow("""Results.CreatedAtRoute("", null, (object) bar)""")]
+ [DataRow("""Results.CreatedAtRoute(value: bar)""")]
+ [DataRow("""Results.CreatedAtRoute("", null, bar)""")]
+ [DataRow("""Results.Accepted("uri", bar)""")]
+ [DataRow("""Results.Accepted("uri", (object) bar)""")]
+ [DataRow("""Results.AcceptedAtRoute(value: (object) bar)""")]
+ [DataRow("""Results.AcceptedAtRoute("", null, (object) bar)""")]
+ [DataRow("""Results.AcceptedAtRoute(value: bar)""")]
+ [DataRow("""Results.AcceptedAtRoute("", null, bar)""")]
+ public void SwaggerActionReturnType_IResult(string invocation) =>
+ builder
+ .AddSnippet($$"""
+ using System;
+ using Microsoft.AspNetCore.Mvc;
+ using Microsoft.AspNetCore.Http;
+
+ [ApiController]
+ public class Foo : Controller
+ {
+ [HttpGet("a")]
+ public IResult Method() => // Noncompliant
+ {{invocation}}; // Secondary
+
+ private Bar bar = new();
+ }
+
+ public class Bar {}
+ """)
+ .Verify();
+
+ [TestMethod]
+ public void ApiConventionType_AssemblyLevel() =>
+ builder
+ .AddSnippet("""
+ using Microsoft.AspNetCore.Mvc;
+
+ [assembly: ApiConventionType(typeof(DefaultApiConventions))]
+ namespace MyNameSpace;
+
+ [ApiController]
+ public class Foo : Controller
+ {
+ [HttpGet("a")]
+ public IActionResult Method() => Ok(bar); // Compliant
+ private Bar bar = new();
+ }
+
+ public class Bar {}
+ """)
+ .Verify();
+}
+
+#endif
diff --git a/analyzers/tests/SonarAnalyzer.Test/TestCases/SwaggerActionReturnType.cs b/analyzers/tests/SonarAnalyzer.Test/TestCases/SwaggerActionReturnType.cs
new file mode 100644
index 00000000000..8b88b8dad79
--- /dev/null
+++ b/analyzers/tests/SonarAnalyzer.Test/TestCases/SwaggerActionReturnType.cs
@@ -0,0 +1,280 @@
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.HttpResults;
+using Microsoft.AspNetCore.Mvc;
+using Swashbuckle.AspNetCore.Annotations;
+
+[ApiController]
+public class CompliantBaseline : Controller
+{
+ private Foo foo = new();
+
+ [HttpGet("foo")]
+ public IActionResult ReturnsNoValue() => Ok();
+
+ [HttpGet("foo")]
+ public IActionResult NotSuccessfulResult() => BadRequest(foo);
+
+ [HttpGet("foo")]
+ [ProducesResponseType(typeof(Foo), StatusCodes.Status200OK)]
+ public IActionResult HasProducesResponseTypeTypeOf() => Ok(foo);
+
+ [HttpGet("foo")]
+ [ProducesResponseType(typeof(Foo), StatusCodes.Status200OK, "application/json")]
+ public IActionResult HasProducesResponseTypeTypeOf2() => Ok(foo);
+
+ [HttpGet("foo")]
+ [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(int))]
+ public IActionResult HasProducesResponseTypeAsParameter() => Ok(foo);
+
+ [HttpGet("foo")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public IActionResult HasProducesResponseTypeGeneric() => Ok(foo);
+
+ [HttpGet("foo")]
+ [ProducesResponseType(StatusCodes.Status200OK, "application/json")]
+ public IActionResult HasProducesResponseTypeGeneric2() => Ok(foo);
+
+ [HttpGet("foo")]
+ [SwaggerResponse(StatusCodes.Status200OK, "", typeof(Foo), "application/json")]
+ public IActionResult HasSwaggerResponse() => Ok(foo);
+
+ [HttpGet("foo")]
+ [SwaggerResponse(StatusCodes.Status200OK, type: typeof(Foo))]
+ public IActionResult HasSwaggerResponse2() => Ok(foo);
+
+ [HttpGet("foo")]
+ [SwaggerResponse(StatusCodes.Status200OK, type: typeof(Foo))]
+ public IResult IResult_HasAnnotation() => Results.Ok(foo);
+
+ [HttpGet("foo")]
+ public ActionResult TypedResponse1() => Ok(foo);
+
+ [HttpGet("foo")]
+ public Foo TypedResponse2() => foo;
+
+ [HttpGet("foo")]
+ public Results> TypedResponse3() =>
+ foo == null
+ ? TypedResults.NotFound()
+ : TypedResults.Ok(foo);
+
+ [HttpGet("foo")]
+ public async Task TypedResponse4()
+ {
+ await Task.Delay(42);
+ return foo;
+ }
+
+ // For implementation: I think if the type is specified at least once, we should not raise for simplicity, even if there is an http code mismatch.
+ [Route("foo")]
+ [ProducesResponseType(StatusCodes.Status201Created)]
+ public IActionResult AnnotatedForWrongStatusCode()
+ {
+ return Ok(foo); // This raises API1000, so the user will find out that the status code in the attribute is wrong.
+ }
+
+ [HttpGet("foo")]
+ [ApiExplorerSettings(IgnoreApi = true)]
+ public IActionResult IgnoreApiTrue() => Ok(42);
+}
+
+[ApiController]
+public class NocompliantBaseline : ControllerBase
+{
+ private Foo foo = new();
+
+ // For the implementation: If this seems too cumbersome, consider dropping it and documenting it as FN
+ [HttpGet("foo")]
+ public ObjectResult NewObjectResult() => // Noncompliant {{Annotate this method with ProducesResponseType containing the return type for successful responses.}}
+ // ^^^^^^^^^^^^^^^
+ new ObjectResult(42); // Secondary
+ // ^^^^^^^^^^^^^^^^^^^^
+
+ [HttpGet("foo")]
+ public IActionResult ReturnsOkWithValue() // Noncompliant {{Annotate this method with ProducesResponseType containing the return type for successful responses.}}
+ // ^^^^^^^^^^^^^^^^^^
+ {
+ return Ok(foo); // Secondary
+ // ^^^^^^^
+ }
+
+ [HttpGet("foo")]
+ public IActionResult ReturnsMultipleValues(bool condition) // Noncompliant {{Annotate this method with ProducesResponseType containing the return type for successful responses.}}
+ // ^^^^^^^^^^^^^^^^^^^^^
+ {
+ if (condition)
+ {
+ return Ok(foo); // Secondary
+ // ^^^^^^^
+ }
+ else
+ {
+ return Accepted(foo); // Secondary
+ // ^^^^^^^^^^^^^
+ }
+ }
+
+ [HttpGet("foo")]
+ public IActionResult ReturnsMultipleValuesTernary() // Noncompliant {{Annotate this method with ProducesResponseType containing the return type for successful responses.}}
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ {
+ return true
+ ? Ok(foo) // Secondary
+ // ^^^^^^^
+ : Accepted(foo); // Secondary
+ // ^^^^^^^^^^^^^
+ }
+
+ [HttpGet("foo")]
+ public IActionResult ReturnsMultipleValuesSwitch(int id) // Noncompliant {{Annotate this method with ProducesResponseType containing the return type for successful responses.}}
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ {
+ return id switch
+ {
+ 1 => Ok(foo), // Secondary
+ // ^^^^^^^
+ 2 => BadRequest(),
+ 3 => Accepted(foo), // Secondary
+ // ^^^^^^^^^^^^^
+ 4 => NotFound(),
+ 5 => Created("", foo), // Secondary
+ // ^^^^^^^^^^^^^^^^
+ };
+ }
+
+ [Route("foo")]
+ public IActionResult MarkedWithRoute() // Noncompliant {{Annotate this method with ProducesResponseType containing the return type for successful responses.}}
+ // ^^^^^^^^^^^^^^^
+ {
+ return Ok(foo); // Secondary
+ // ^^^^^^^
+ }
+
+ [Route("foo")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public IActionResult AnnotatedWithNoType() // Noncompliant {{Use the ProducesResponseType overload containing the return type for successful responses.}}
+ // ^^^^^^^^^^^^^^^^^^^
+ {
+ return Ok(foo); // Secondary
+ // ^^^^^^^
+ }
+
+ [Route("foo")]
+ [SwaggerResponse(200)]
+ public IActionResult AnnotatedWithNoType2() // Noncompliant {{Use the ProducesResponseType overload containing the return type for successful responses.}}
+ // ^^^^^^^^^^^^^^^^^^^^
+ {
+ return Ok(foo); // Secondary
+ // ^^^^^^^
+ }
+
+ [HttpGet("foo")]
+ [ApiExplorerSettings(IgnoreApi = false)]
+ public IActionResult IgnoreApiFalse() => // Noncompliant
+ Ok(42); // Secondary
+
+ [HttpGet("foo")]
+ public IActionResult UnusedSuccessResponse() // Noncompliant - FP, the created success response is not returned
+ {
+ var x = Ok(42); // Secondary
+ return null;
+ }
+}
+
+public class NotApiController : Controller
+{
+ [HttpGet("foo")]
+ public IActionResult Foo() => Ok(new Foo());
+}
+
+[NonController]
+[ApiController]
+public class NotAController : Controller
+{
+ [HttpGet("foo")]
+ public IActionResult Foo() => Ok(new Foo());
+}
+
+[ApiController]
+public class NotPublicAction : Controller
+{
+ [HttpGet("foo")]
+ internal IActionResult Foo() => Ok(new Foo());
+}
+
+[ApiController]
+public class UsesApiConventionMethod : Controller
+{
+ [HttpGet("foo")]
+ [ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Get))]
+ public IActionResult Foo() => Ok(new Foo());
+}
+
+[ApiController]
+[ApiConventionType(typeof(DefaultApiConventions))]
+public class UsesApiConventionType : ControllerBase
+{
+ [HttpGet("foo")]
+ public IActionResult Foo() => Ok(new Foo());
+}
+
+[ApiController]
+[ProducesResponseType(StatusCodes.Status200OK)]
+public class AnnotatedAtControllerLevel : ControllerBase
+{
+ [HttpGet("foo")]
+ public IActionResult ReturnsOkWithValue() => Ok(42);
+}
+
+[ApiController]
+[ProducesResponseType(typeof(int), StatusCodes.Status200OK)]
+public class AnnotatedAtControllerLevelWithTypeOf : ControllerBase
+{
+ [HttpGet("foo")]
+ public IActionResult ReturnsOkWithValue() => Ok(42);
+}
+
+[ApiController]
+[ProducesResponseType(200)]
+public class AnnotatedAtControllerLevelWithNoType : ControllerBase
+{
+ [HttpGet("foo")]
+ public IActionResult ReturnsOkWithValue() => // Noncompliant {{Annotate this method with ProducesResponseType containing the return type for successful responses.}}
+ // ^^^^^^^^^^^^^^^^^^
+ Ok(42); // Secondary
+ // ^^^^^^
+
+}
+
+[ApiController]
+[ApiExplorerSettings(IgnoreApi = true)]
+public class IgnoreApiTrueController : ControllerBase
+{
+ [HttpGet("foo")]
+ public IActionResult ReturnsOkWithValue() => Ok(42);
+}
+
+[ApiController]
+[SwaggerResponse(StatusCodes.Status200OK, type: typeof(Foo))]
+public class SwaggerResponseController : Controller
+{
+ [HttpGet("foo")]
+ public IActionResult Foo() => Ok(new Foo());
+}
+
+[ApiController]
+[ApiExplorerSettings(IgnoreApi = false)]
+public class IgnoreApiFalseController : ControllerBase
+{
+ [HttpGet("foo")]
+ public IActionResult ReturnsOkWithValue() => // Noncompliant {{Annotate this method with ProducesResponseType containing the return type for successful responses.}}
+ // ^^^^^^^^^^^^^^^^^^
+ Ok(42); // Secondary
+ // ^^^^^^
+}
+
+public class Foo
+{
+ public int Bar { get; set; }
+}
diff --git a/analyzers/tests/SonarAnalyzer.TestFramework/MetadataReferences/AspNetCoreMetadataReference.cs b/analyzers/tests/SonarAnalyzer.TestFramework/MetadataReferences/AspNetCoreMetadataReference.cs
index 75ed60564ff..47e3866773d 100644
--- a/analyzers/tests/SonarAnalyzer.TestFramework/MetadataReferences/AspNetCoreMetadataReference.cs
+++ b/analyzers/tests/SonarAnalyzer.TestFramework/MetadataReferences/AspNetCoreMetadataReference.cs
@@ -33,6 +33,7 @@ public static class AspNetCoreMetadataReference
public static MetadataReference MicrosoftAspNetCoreHostingAbstractions { get; } = CreateReference("Microsoft.AspNetCore.Hosting.Abstractions.dll", Sdk.AspNetCore);
public static MetadataReference MicrosoftAspNetCoreHttpAbstractions { get; } = CreateReference("Microsoft.AspNetCore.Http.Abstractions.dll", Sdk.AspNetCore);
public static MetadataReference MicrosoftAspNetCoreHttpFeatures { get; } = CreateReference("Microsoft.AspNetCore.Http.Features.dll", Sdk.AspNetCore);
+ public static MetadataReference MicrosoftAspNetCoreHttpResults { get; } = CreateReference("Microsoft.AspNetCore.Http.Results.dll", Sdk.AspNetCore);
public static MetadataReference MicrosoftAspNetCoreMvc { get; } = CreateReference("Microsoft.AspNetCore.Mvc.dll", Sdk.AspNetCore);
public static MetadataReference MicrosoftAspNetCoreMvcAbstractions { get; } = CreateReference("Microsoft.AspNetCore.Mvc.Abstractions.dll", Sdk.AspNetCore);
public static MetadataReference MicrosoftAspNetCoreMvcCore { get; } = CreateReference("Microsoft.AspNetCore.Mvc.Core.dll", Sdk.AspNetCore);
diff --git a/analyzers/tests/SonarAnalyzer.TestFramework/MetadataReferences/NuGetMetadataReference.cs b/analyzers/tests/SonarAnalyzer.TestFramework/MetadataReferences/NuGetMetadataReference.cs
index a07c28e4bcf..546c787b0fa 100644
--- a/analyzers/tests/SonarAnalyzer.TestFramework/MetadataReferences/NuGetMetadataReference.cs
+++ b/analyzers/tests/SonarAnalyzer.TestFramework/MetadataReferences/NuGetMetadataReference.cs
@@ -162,6 +162,8 @@ public static class NuGetMetadataReference
public static References SystemTextRegularExpressions(string packageVersion = "4.3.1") => Create("System.Text.RegularExpressions", packageVersion);
public static References SystemThreadingTasksExtensions(string packageVersion) => Create("System.Threading.Tasks.Extensions", packageVersion);
public static References SystemValueTuple(string packageVersion) => Create("System.ValueTuple", packageVersion);
+ public static References SwashbuckleAspNetCoreAnnotations(string packageVersion = Constants.NuGetLatestVersion) => Create("Swashbuckle.AspNetCore.Annotations", packageVersion);
+ public static References SwashbuckleAspNetCoreSwagger(string packageVersion = Constants.NuGetLatestVersion) => Create("Swashbuckle.AspNetCore.Swagger", packageVersion);
public static References TimeZoneConverter(string packageVersion = Constants.NuGetLatestVersion) => Create("TimeZoneConverter", packageVersion);
public static References XunitFramework(string packageVersion) =>
Create("xunit.assert", packageVersion)