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:

+ +

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)