diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/AspNet/AvoidUnderPosting.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/AspNet/AvoidUnderPosting.cs index 1b4d72415dd..834e6e4128b 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Rules/AspNet/AvoidUnderPosting.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Rules/AspNet/AvoidUnderPosting.cs @@ -33,6 +33,12 @@ public sealed class AvoidUnderPosting : SonarDiagnosticAnalyzer KnownType.Microsoft_AspNetCore_Http_IFormCollection, KnownType.Microsoft_AspNetCore_Http_IFormFile, KnownType.Microsoft_AspNetCore_Http_IFormFileCollection); + private static readonly ImmutableArray IgnoredAttributes = ImmutableArray.Create( + KnownType.System_Text_Json_Serialization_JsonIgnoreAttribute, + KnownType.System_Text_Json_Serialization_JsonRequiredAttribute, + KnownType.Newtonsoft_Json_JsonIgnoreAttribute, + KnownType.Newtonsoft_Json_JsonRequiredAttribute, + KnownType.System_ComponentModel_DataAnnotations_RangeAttribute); public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); @@ -76,13 +82,24 @@ private static void CheckInvalidProperties(INamedTypeSymbol parameterType, Sonar var declaredProperties = new List(); GetAllDeclaredProperties(parameterType, examinedTypes, declaredProperties); var invalidProperties = declaredProperties - .Where(x => !CanBeNull(x.Type) && !x.HasAttribute(KnownType.System_Text_Json_Serialization_JsonRequiredAttribute) && !x.IsRequired()) + .Where(x => !IsExcluded(x)) .Select(x => x.GetFirstSyntaxRef()) .Where(x => !IsInitialized(x)); foreach (var property in invalidProperties) { context.ReportIssue(Rule, property.GetIdentifier()?.GetLocation()); } + + static bool IsExcluded(IPropertySymbol property) => + CanBeNull(property.Type) + || property.HasAnyAttribute(IgnoredAttributes) + || IsNewtonsoftJsonPropertyRequired(property) + || property.IsRequired(); + + static bool IsNewtonsoftJsonPropertyRequired(IPropertySymbol property) => + property.GetAttributes(KnownType.Newtonsoft_Json_JsonPropertyAttribute).FirstOrDefault() is { } attribute + && attribute.TryGetAttributeValue("Required", out int required) + && (required is 1 or 2); // Required.AllowNull = 1, Required.Always = 2, https://www.newtonsoft.com/json/help/html/T_Newtonsoft_Json_Required.htm } private static bool IgnoreType(ITypeSymbol type) => diff --git a/analyzers/src/SonarAnalyzer.Common/Extensions/ISymbolExtensions.cs b/analyzers/src/SonarAnalyzer.Common/Extensions/ISymbolExtensions.cs index b0dbb0c6130..df8dc3291b8 100644 --- a/analyzers/src/SonarAnalyzer.Common/Extensions/ISymbolExtensions.cs +++ b/analyzers/src/SonarAnalyzer.Common/Extensions/ISymbolExtensions.cs @@ -24,6 +24,9 @@ namespace SonarAnalyzer.Extensions; public static class ISymbolExtensions { + public static bool HasAnyAttribute(this ISymbol symbol, ImmutableArray types) => + symbol.GetAttributes(types).Any(); + public static bool HasAttribute(this ISymbol symbol, KnownType type) => symbol.GetAttributes(type).Any(); diff --git a/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs b/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs index d109a317784..5935bc569bc 100644 --- a/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs +++ b/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs @@ -202,6 +202,9 @@ public sealed partial class KnownType public static readonly KnownType NLog_LogFactory = new("NLog.LogFactory"); public static readonly KnownType NLog_LogManager = new("NLog.LogManager"); public static readonly KnownType NLog_Logger = new("NLog.Logger"); + public static readonly KnownType Newtonsoft_Json_JsonPropertyAttribute = new("Newtonsoft.Json.JsonPropertyAttribute"); + public static readonly KnownType Newtonsoft_Json_JsonRequiredAttribute = new("Newtonsoft.Json.JsonRequiredAttribute"); + public static readonly KnownType Newtonsoft_Json_JsonIgnoreAttribute = new("Newtonsoft.Json.JsonIgnoreAttribute"); public static readonly KnownType NUnit_Framework_Assert = new("NUnit.Framework.Assert"); public static readonly KnownType NUnit_Framework_AssertionException = new("NUnit.Framework.AssertionException"); public static readonly KnownType NUnit_Framework_ExpectedExceptionAttribute = new("NUnit.Framework.ExpectedExceptionAttribute"); @@ -321,6 +324,7 @@ public sealed partial class KnownType public static readonly KnownType System_ComponentModel_Composition_PartCreationPolicyAttribute = new("System.ComponentModel.Composition.PartCreationPolicyAttribute"); public static readonly KnownType System_ComponentModel_DataAnnotations_KeyAttribute = new("System.ComponentModel.DataAnnotations.KeyAttribute"); public static readonly KnownType System_ComponentModel_DataAnnotations_RegularExpressionAttribute = new("System.ComponentModel.DataAnnotations.RegularExpressionAttribute"); + public static readonly KnownType System_ComponentModel_DataAnnotations_RangeAttribute = new("System.ComponentModel.DataAnnotations.RangeAttribute"); public static readonly KnownType System_ComponentModel_DataAnnotations_IValidatableObject = new("System.ComponentModel.DataAnnotations.IValidatableObject"); public static readonly KnownType System_ComponentModel_DataAnnotations_RequiredAttribute = new("System.ComponentModel.DataAnnotations.RequiredAttribute"); public static readonly KnownType System_ComponentModel_DataAnnotations_ValidationAttribute = new("System.ComponentModel.DataAnnotations.ValidationAttribute"); @@ -561,6 +565,7 @@ public sealed partial class KnownType public static readonly KnownType System_StringComparison = new("System.StringComparison"); public static readonly KnownType System_SystemException = new("System.SystemException"); public static readonly KnownType System_Text_Encoding = new("System.Text.Encoding"); + public static readonly KnownType System_Text_Json_Serialization_JsonIgnoreAttribute = new("System.Text.Json.Serialization.JsonIgnoreAttribute"); public static readonly KnownType System_Text_Json_Serialization_JsonRequiredAttribute = new("System.Text.Json.Serialization.JsonRequiredAttribute"); public static readonly KnownType System_Text_RegularExpressions_Regex = new("System.Text.RegularExpressions.Regex"); public static readonly KnownType System_Text_RegularExpressions_RegexOptions = new("System.Text.RegularExpressions.RegexOptions"); diff --git a/analyzers/tests/SonarAnalyzer.Test/Rules/AspNet/AvoidUnderPostingTest.cs b/analyzers/tests/SonarAnalyzer.Test/Rules/AspNet/AvoidUnderPostingTest.cs index 71c3200ff68..154d0fce7c6 100644 --- a/analyzers/tests/SonarAnalyzer.Test/Rules/AspNet/AvoidUnderPostingTest.cs +++ b/analyzers/tests/SonarAnalyzer.Test/Rules/AspNet/AvoidUnderPostingTest.cs @@ -35,7 +35,7 @@ public class AvoidUnderPostingTest private readonly VerifierBuilder builder = new VerifierBuilder() .WithBasePath("AspNet") - .AddReferences([..AspNetReferences, .. NuGetMetadataReference.SystemTextJson("7.0.4")]); + .AddReferences([..AspNetReferences, .. NuGetMetadataReference.SystemTextJson("7.0.4"), ..NuGetMetadataReference.NewtonsoftJson("13.0.3")]); [TestMethod] public void AvoidUnderPosting_CSharp() => diff --git a/analyzers/tests/SonarAnalyzer.Test/TestCases/AspNet/AvoidUnderPosting.cs b/analyzers/tests/SonarAnalyzer.Test/TestCases/AspNet/AvoidUnderPosting.cs index c5f5a6e82b0..f7bdf1236b5 100644 --- a/analyzers/tests/SonarAnalyzer.Test/TestCases/AspNet/AvoidUnderPosting.cs +++ b/analyzers/tests/SonarAnalyzer.Test/TestCases/AspNet/AvoidUnderPosting.cs @@ -1,5 +1,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; +using Newtonsoft.Json; +using System.Text.Json.Serialization; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -17,8 +19,18 @@ public class ModelUsedInController // ^^^^^^^^^^^^^ public int? NullableValueProperty { get; set; } [Required] public int RequiredValueProperty { get; set; } // Noncompliant, RequiredAttribute has no effect on value types - [Range(0, 10)] public int ValuePropertyWithRangeValidation { get; set; } // Noncompliant + [Range(0, 10)] public int ValuePropertyWithRangeValidation { get; set; } // Compliant [Required] public int? RequiredNullableValueProperty { get; set; } + [JsonProperty(Required = Required.Always)] public int JsonRequiredValuePropertyAlways { get; set; } // Compliant + [JsonProperty(Required = Required.AllowNull)] public int JsonRequiredValuePropertyAllowNull { get; set; } // Compliant + [JsonProperty(Required = Required.DisallowNull)] public int JsonRequiredValuePropertyDisallowNull { get; set; } // Noncompliant + [JsonProperty] public int JsonRequiredValuePropertyDefault { get; set; } // Noncompliant + [Newtonsoft.Json.JsonIgnore] public int JsonIgnoredProperty { get; set; } // Compliant + [Newtonsoft.Json.JsonRequired] public int JsonRequiredNewtonsoftValueProperty { get; set; } // Compliant + [System.Text.Json.Serialization.JsonRequired] public int JsonRequiredValueProperty { get; set; } // Compliant + [System.Text.Json.Serialization.JsonIgnore] public int JsonIgnoreValueProperty { get; set; } // Compliant + [JsonProperty(Required = Required.AllowNull)] [FromQuery] public int PropertyWithMultipleAttributesCompliant { get; set; } // Compliant + [Required] [FromQuery] public int PropertyWithMultipleAttributesNonCompliant { get; set; } // Noncompliant public int PropertyWithPrivateSetter { get; private set; } protected int ProtectedProperty { get; set; } internal int InternalProperty { get; set; } diff --git a/analyzers/tests/SonarAnalyzer.TestFramework/MetadataReferences/NuGetMetadataReference.cs b/analyzers/tests/SonarAnalyzer.TestFramework/MetadataReferences/NuGetMetadataReference.cs index 4395841f40d..22af1b3569f 100644 --- a/analyzers/tests/SonarAnalyzer.TestFramework/MetadataReferences/NuGetMetadataReference.cs +++ b/analyzers/tests/SonarAnalyzer.TestFramework/MetadataReferences/NuGetMetadataReference.cs @@ -139,6 +139,7 @@ public static References MongoDBDriver(string packageVersion = Constants.NuGetLa public static References NHibernate(string packageVersion = "5.2.2") => Create("NHibernate", packageVersion); public static References NpgsqlEntityFrameworkCorePostgreSQL(string packageVersion) => Create("Npgsql.EntityFrameworkCore.PostgreSQL", packageVersion); public static References NSubstitute(string packageVersion) => Create("NSubstitute", packageVersion); + public static References NewtonsoftJson(string packageVersion) => Create("Newtonsoft.Json", packageVersion); public static References NUnit(string packageVersion) => Create("NUnit", packageVersion); public static References NUnitLite(string packageVersion) => Create("NUnitLite", packageVersion); public static References OracleEntityFrameworkCore(string packageVersion) => Create("Oracle.EntityFrameworkCore", packageVersion);