diff --git a/analyzers/its/expected/Net5/Net5--net5.0-S2436.json b/analyzers/its/expected/Net5/Net5--net5.0-S2436.json index 1da2b5c681b..4425c85b4cb 100644 --- a/analyzers/its/expected/Net5/Net5--net5.0-S2436.json +++ b/analyzers/its/expected/Net5/Net5--net5.0-S2436.json @@ -2,7 +2,7 @@ "issues": [ { "id": "S2436", -"message": "Reduce the number of generic parameters in the '.LocalBar' method to no more than the 3 authorized.", +"message": "Reduce the number of generic parameters in the 'LocalBar' method to no more than the 3 authorized.", "location": { "uri": "sources\Net5\Net5\Main.cs", "region": { diff --git a/analyzers/src/SonarAnalyzer.CSharp/Extensions/SyntaxNodeExtensions.cs b/analyzers/src/SonarAnalyzer.CSharp/Extensions/SyntaxNodeExtensions.cs index 1111611bdf9..69903ed35a0 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Extensions/SyntaxNodeExtensions.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Extensions/SyntaxNodeExtensions.cs @@ -91,6 +91,7 @@ public static string GetDeclarationTypeName(this SyntaxNode node) => SyntaxKind.StructDeclaration => "struct", SyntaxKind.InterfaceDeclaration => "interface", SyntaxKindEx.RecordClassDeclaration => "record", + SyntaxKindEx.RecordStructDeclaration => "record struct", _ => GetUnknownType(node.Kind()) }; diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/TooManyGenericParameters.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/TooManyGenericParameters.cs index 410a1814de9..d58785edc25 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Rules/TooManyGenericParameters.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Rules/TooManyGenericParameters.cs @@ -19,6 +19,7 @@ */ using System.Collections.Immutable; +using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -56,20 +57,24 @@ protected override void Initialize(ParameterLoadingAnalysisContext context) { var typeDeclaration = (TypeDeclarationSyntax)c.Node; - if (c.ContainingSymbol.Kind != SymbolKind.NamedType + if (c.IsRedundantPositionalRecordContext() || typeDeclaration.TypeParameterList == null || typeDeclaration.TypeParameterList.Parameters.Count <= MaxNumberOfGenericParametersInClass) { return; } - c.ReportIssue(Diagnostic.Create(Rule, typeDeclaration.Identifier.GetLocation(), - typeDeclaration.Identifier.ValueText, typeDeclaration.GetDeclarationTypeName(), MaxNumberOfGenericParametersInClass)); + c.ReportIssue(Diagnostic.Create(Rule, + typeDeclaration.Identifier.GetLocation(), + typeDeclaration.Identifier.ValueText, + typeDeclaration.GetDeclarationTypeName(), + MaxNumberOfGenericParametersInClass)); }, SyntaxKind.ClassDeclaration, SyntaxKind.StructDeclaration, SyntaxKind.InterfaceDeclaration, - SyntaxKindEx.RecordClassDeclaration); + SyntaxKindEx.RecordClassDeclaration, + SyntaxKindEx.RecordStructDeclaration); context.RegisterSyntaxNodeActionInNonGenerated( c => @@ -81,43 +86,17 @@ protected override void Initialize(ParameterLoadingAnalysisContext context) return; } - c.ReportIssue(Diagnostic.Create( - Rule, - methodDeclaration.Identifier.GetLocation(), - $"{GetEnclosingTypeName(c.Node)}.{methodDeclaration.Identifier.ValueText}", - "method", - MaxNumberOfGenericParametersInMethod)); + c.ReportIssue(Diagnostic.Create(Rule, + methodDeclaration.Identifier.GetLocation(), + new[] { EnclosingTypeName(c.Node), methodDeclaration.Identifier.ValueText }.JoinNonEmpty("."), + "method", + MaxNumberOfGenericParametersInMethod)); }, SyntaxKind.MethodDeclaration, SyntaxKindEx.LocalFunctionStatement); } - private static string GetEnclosingTypeName(SyntaxNode node) - { - var parent = node.Parent; - - while (parent != null) - { - switch (parent.Kind()) - { - case SyntaxKind.ClassDeclaration: - return ((ClassDeclarationSyntax)parent).Identifier.ValueText; - - case SyntaxKind.StructDeclaration: - return ((StructDeclarationSyntax)parent).Identifier.ValueText; - - case SyntaxKind.InterfaceDeclaration: - return ((InterfaceDeclarationSyntax)parent).Identifier.ValueText; - - case SyntaxKindEx.RecordClassDeclaration: - return ((RecordDeclarationSyntaxWrapper)parent).Identifier.ValueText; - - default: - parent = parent.Parent; - break; - } - } - return null; - } + private static string EnclosingTypeName(SyntaxNode node) => + node.Ancestors().OfType<BaseTypeDeclarationSyntax>().FirstOrDefault()?.Identifier.ValueText; } } diff --git a/analyzers/src/SonarAnalyzer.Common/Helpers/EnumerableExtensions.cs b/analyzers/src/SonarAnalyzer.Common/Helpers/EnumerableExtensions.cs index 4871f26b763..29fb92fe5ab 100644 --- a/analyzers/src/SonarAnalyzer.Common/Helpers/EnumerableExtensions.cs +++ b/analyzers/src/SonarAnalyzer.Common/Helpers/EnumerableExtensions.cs @@ -118,29 +118,36 @@ public static int IndexOf<T>(this IEnumerable<T> enumerable, Func<T, bool> predi ?.index ?? -1; /// <summary> - /// This is string.Join() as extension. Concatenates members of collection using specified separator between each member. Selector is used to extract string value from T for concatenation. + /// This is <see cref="string.Join"/> as extension. It concatenates the members of the collection using the specified <paramref name="separator"/> between each member. + /// <paramref name="selector"/> is used to convert <typeparamref name="T"/> to <see cref="string"/> for concatenation. /// </summary> public static string JoinStr<T>(this IEnumerable<T> enumerable, string separator, Func<T, string> selector) => string.Join(separator, enumerable.Select(x => selector(x))); /// <summary> - /// This is string.Join() as extension. Concatenates members of collection using specified separator between each member. Selector is used to extract integer value from T for concatenation. + /// This is <see cref="string.Join"/> as extension. It concatenates the members of the collection using the specified <paramref name="separator"/> between each member. + /// <paramref name="selector"/> is used to convert <typeparamref name="T"/> to <see cref="int"/> for concatenation. /// </summary> public static string JoinStr<T>(this IEnumerable<T> enumerable, string separator, Func<T, int> selector) => string.Join(separator, enumerable.Select(x => selector(x))); /// <summary> - /// This is string.Join() as extension. Concatenates members of string collection using specified separator between each member. + /// This is <see cref="string.Join"/> as extension. It concatenates the members of the collection using the specified <paramref name="separator"/> between each member. /// </summary> public static string JoinStr(this IEnumerable<string> enumerable, string separator) => JoinStr(enumerable, separator, x => x); /// <summary> - /// This is string.Join() as extension. Concatenates members of int collection using specified separator between each member. + /// This is <see cref="string.Join"/> as extension. It concatenates the members of the collection using the specified <paramref name="separator"/> between each member. /// </summary> public static string JoinStr(this IEnumerable<int> enumerable, string separator) => JoinStr(enumerable, separator, x => x); - + /// <summary> + /// Concatenates the members of a <see cref="string"/> collection using the specified <paramref name="separator"/> between each member. + /// Any whitespace or null member of the collection will be ignored. + /// </summary> + public static string JoinNonEmpty(this IEnumerable<string> enumerable, string separator) => + string.Join(separator, enumerable.Where(x => !string.IsNullOrWhiteSpace(x))); } } diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/Helpers/EnumerableExtensionsTest.cs b/analyzers/tests/SonarAnalyzer.UnitTest/Helpers/EnumerableExtensionsTest.cs index 3d0414f3a29..4f58a100ccc 100644 --- a/analyzers/tests/SonarAnalyzer.UnitTest/Helpers/EnumerableExtensionsTest.cs +++ b/analyzers/tests/SonarAnalyzer.UnitTest/Helpers/EnumerableExtensionsTest.cs @@ -18,6 +18,8 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +#pragma warning disable SA1122 // Use string.Empty for empty strings + using System; using System.Collections.Generic; using FluentAssertions; @@ -131,6 +133,23 @@ public void JoinStr_Int() new[] { 1, 22, 333 }.JoinStr(null).Should().Be("122333"); } + [TestMethod] + public void JoinNonEmpty() + { + Array.Empty<string>().JoinNonEmpty(", ").Should().Be(""); + new[] { "a" }.JoinNonEmpty(", ").Should().Be("a"); + new[] { "a", "bb", "ccc" }.JoinNonEmpty(", ").Should().Be("a, bb, ccc"); + new[] { "a", "bb", "ccc" }.JoinNonEmpty(null).Should().Be("abbccc"); + new[] { "a", "bb", "ccc" }.JoinNonEmpty("").Should().Be("abbccc"); + new[] { null, "a", "b" }.JoinNonEmpty(".").Should().Be("a.b"); + new[] { "a", null, "b" }.JoinNonEmpty(".").Should().Be("a.b"); + new[] { "a", "b", null }.JoinNonEmpty(".").Should().Be("a.b"); + new string[] { null, null, null }.JoinNonEmpty(".").Should().Be(""); + new string[] { "", "", "" }.JoinNonEmpty(".").Should().Be(""); + new string[] { "", "\t", " " }.JoinNonEmpty(".").Should().Be(""); + new string[] { "a", "\t", "b" }.JoinNonEmpty(".").Should().Be("a.b"); + } + [TestMethod] public void WhereNotNull_Class() { @@ -138,7 +157,7 @@ public void WhereNotNull_Class() Array.Empty<object>().WhereNotNull().Should().BeEmpty(); new object[] { null, null, null }.WhereNotNull().Should().BeEmpty(); new object[] { 1, "a", instance }.WhereNotNull().Should().BeEquivalentTo(new object[] { 1, "a", instance }); - new object[] { 1, "a", null }.WhereNotNull().Should().BeEquivalentTo(new object[] { 1, "a"}); + new object[] { 1, "a", null }.WhereNotNull().Should().BeEquivalentTo(new object[] { 1, "a" }); } [TestMethod] @@ -162,3 +181,5 @@ public StructType(int count) } } } + +#pragma warning restore SA1122 // Use string.Empty for empty strings diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/Rules/TooManyGenericParametersTest.cs b/analyzers/tests/SonarAnalyzer.UnitTest/Rules/TooManyGenericParametersTest.cs index b94382a6ee7..309e04c5560 100644 --- a/analyzers/tests/SonarAnalyzer.UnitTest/Rules/TooManyGenericParametersTest.cs +++ b/analyzers/tests/SonarAnalyzer.UnitTest/Rules/TooManyGenericParametersTest.cs @@ -27,23 +27,27 @@ namespace SonarAnalyzer.UnitTest.Rules [TestClass] public class TooManyGenericParametersTest { + private readonly VerifierBuilder builder = new VerifierBuilder<TooManyGenericParameters>(); + [TestMethod] public void TooManyGenericParameters_DefaultValues() => - OldVerifier.VerifyAnalyzer(@"TestCases\TooManyGenericParameters_DefaultValues.cs", new TooManyGenericParameters()); + builder.AddPaths("TooManyGenericParameters.DefaultValues.cs").Verify(); [TestMethod] public void TooManyGenericParameters_CustomValues() => - OldVerifier.VerifyAnalyzer(@"TestCases\TooManyGenericParameters_CustomValues.cs", - new TooManyGenericParameters { MaxNumberOfGenericParametersInClass = 4, MaxNumberOfGenericParametersInMethod = 4 }); + new VerifierBuilder() + .AddAnalyzer(() => new TooManyGenericParameters { MaxNumberOfGenericParametersInClass = 4, MaxNumberOfGenericParametersInMethod = 4 }) + .AddPaths("TooManyGenericParameters.CustomValues.cs") + .Verify(); #if NET [TestMethod] public void TooManyGenericParameters_CSharp9() => - OldVerifier.VerifyAnalyzerFromCSharp9Console(@"TestCases\TooManyGenericParameters.CSharp9.cs", new TooManyGenericParameters()); + builder.AddPaths("TooManyGenericParameters.CSharp9.cs").WithTopLevelStatements().Verify(); [TestMethod] public void TooManyGenericParameters_CSharp10() => - OldVerifier.VerifyAnalyzerFromCSharp10Console(@"TestCases\TooManyGenericParameters.CSharp10.cs", new TooManyGenericParameters()); + builder.AddPaths("TooManyGenericParameters.CSharp10.cs").WithOptions(ParseOptionsHelper.FromCSharp10).Verify(); #endif } } diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/TooManyGenericParameters.CSharp10.cs b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/TooManyGenericParameters.CSharp10.cs index 4a734e7df97..ef4f4016e3d 100644 --- a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/TooManyGenericParameters.CSharp10.cs +++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/TooManyGenericParameters.CSharp10.cs @@ -1,22 +1,19 @@ -void LocalFoo<T1, T2, T3>() { } -void LocalBar<T1, T2, T3, T4>() { } // Noncompliant {{Reduce the number of generic parameters in the '.LocalBar' method to no more than the 3 authorized.}} - -record struct RecordStruct +record struct RecordStruct { public void Foo<T1, T2, T3>() { } - public void Foo<T1, T2, T3, T4>() { } // Noncompliant {{Reduce the number of generic parameters in the '.Foo' method to no more than the 3 authorized.}} - // Although an issue is raised, the above message is not correct. There should be 'RecordStruct.Foo' instead of '.Foo'. + public void Foo<T1, T2, T3, T4>() { } // Noncompliant {{Reduce the number of generic parameters in the 'RecordStruct.Foo' method to no more than the 3 authorized.}} + // ^^^ } record struct PositionalRecordStruct(int SomeProperty) { public void Foo<T1, T2, T3>() { } - public void Foo<T1, T2, T3, T4>() { } // Noncompliant {{Reduce the number of generic parameters in the '.Foo' method to no more than the 3 authorized.}} - // Although an issue is raised, the above message is not correct. There should be 'PositionalRecordStruct.Foo' instead of '.Foo'. + public void Foo<T1, T2, T3, T4>() { } // Noncompliant {{Reduce the number of generic parameters in the 'PositionalRecordStruct.Foo' method to no more than the 3 authorized.}} } record struct RecordStruct<T1, T2> { } -record struct RecordStruct<T1, T2, T3> { } // FN +record struct RecordStruct<T1, T2, T3> { } // Noncompliant {{Reduce the number of generic parameters in the 'RecordStruct' record struct to no more than the 2 authorized.}} +// ^^^^^^^^^^^^ record struct PositionalRecordStruct<T1, T2>(int SomeProperty) { } -record struct PositionalRecordStruct<T1, T2, T3>(int SomeProperty) { } // FN +record struct PositionalRecordStruct<T1, T2, T3>(int SomeProperty) { } // Noncompliant {{Reduce the number of generic parameters in the 'PositionalRecordStruct' record struct to no more than the 2 authorized.}} diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/TooManyGenericParameters.CSharp9.cs b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/TooManyGenericParameters.CSharp9.cs index 99f43c197bd..b398d4d16a9 100644 --- a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/TooManyGenericParameters.CSharp9.cs +++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/TooManyGenericParameters.CSharp9.cs @@ -1,5 +1,5 @@ void LocalFoo<T1, T2, T3>() { } -void LocalBar<T1, T2, T3, T4>() { } // Noncompliant {{Reduce the number of generic parameters in the '.LocalBar' method to no more than the 3 authorized.}} +void LocalBar<T1, T2, T3, T4>() { } // Noncompliant {{Reduce the number of generic parameters in the 'LocalBar' method to no more than the 3 authorized.}} record Record { diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/TooManyGenericParameters_CustomValues.cs b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/TooManyGenericParameters.CustomValues.cs similarity index 100% rename from analyzers/tests/SonarAnalyzer.UnitTest/TestCases/TooManyGenericParameters_CustomValues.cs rename to analyzers/tests/SonarAnalyzer.UnitTest/TestCases/TooManyGenericParameters.CustomValues.cs diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/TooManyGenericParameters_DefaultValues.cs b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/TooManyGenericParameters.DefaultValues.cs similarity index 100% rename from analyzers/tests/SonarAnalyzer.UnitTest/TestCases/TooManyGenericParameters_DefaultValues.cs rename to analyzers/tests/SonarAnalyzer.UnitTest/TestCases/TooManyGenericParameters.DefaultValues.cs