diff --git a/analyzers/its/expected/Net7/Net7--net7.0-S1144.json b/analyzers/its/expected/Net7/Net7--net7.0-S1144.json index b2788190ee8..9b26fe961e6 100644 --- a/analyzers/its/expected/Net7/Net7--net7.0-S1144.json +++ b/analyzers/its/expected/Net7/Net7--net7.0-S1144.json @@ -54,6 +54,19 @@ }, { "id": "S1144", +"message": "Remove the unused private method 'Method'.", +"location": { +"uri": "sources\Net7\Net7\features\SetPropertiesInsteadOfLinqMethods.cs", +"region": { +"startLine": 8, +"startColumn": 20, +"endLine": 8, +"endColumn": 26 +} +} +}, +{ +"id": "S1144", "message": "Remove the unused internal class 'lowercasename'.", "location": { "uri": "sources\Net7\Net7\features\WarningWave7.cs", diff --git a/analyzers/its/expected/Net7/Net7--net7.0-S1451.json b/analyzers/its/expected/Net7/Net7--net7.0-S1451.json index 70ec1efbd4a..32ecf99ce73 100644 --- a/analyzers/its/expected/Net7/Net7--net7.0-S1451.json +++ b/analyzers/its/expected/Net7/Net7--net7.0-S1451.json @@ -186,6 +186,19 @@ "id": "S1451", "message": "Add or update the header of this file.", "location": { +"uri": "sources\Net7\Net7\features\SetPropertiesInsteadOfLinqMethods.cs", +"region": { +"startLine": 1, +"startColumn": 1, +"endLine": 1, +"endColumn": 1 +} +} +}, +{ +"id": "S1451", +"message": "Add or update the header of this file.", +"location": { "uri": "sources\Net7\Net7\features\StaticVirtualMembersInInterfaces.cs", "region": { "startLine": 1, diff --git a/analyzers/its/expected/Net7/Net7--net7.0-S6609.json b/analyzers/its/expected/Net7/Net7--net7.0-S6609.json new file mode 100644 index 00000000000..025d48a67ab --- /dev/null +++ b/analyzers/its/expected/Net7/Net7--net7.0-S6609.json @@ -0,0 +1,17 @@ +{ +"issues": [ +{ +"id": "S6609", +"message": ""Min" property of Set type should be used instead of the "Min()" extension method.", +"location": { +"uri": "sources\Net7\Net7\features\SetPropertiesInsteadOfLinqMethods.cs", +"region": { +"startLine": 11, +"startColumn": 24, +"endLine": 11, +"endColumn": 27 +} +} +} +] +} diff --git a/analyzers/its/sources/Net7/Net7/features/OrderByBeforeWhere.cs b/analyzers/its/sources/Net7/Net7/features/OrderByBeforeWhere.cs index 4e499a567d4..f1882781df2 100644 --- a/analyzers/its/sources/Net7/Net7/features/OrderByBeforeWhere.cs +++ b/analyzers/its/sources/Net7/Net7/features/OrderByBeforeWhere.cs @@ -1,8 +1,8 @@ namespace Net7.features { - public static class Foo + public static class OrderByBeforeWhere { - public static List Bar() + public static List Method() { var list = new List(); return list.OrderBy(x => x).Where(x => true).ToList(); diff --git a/analyzers/its/sources/Net7/Net7/features/SetPropertiesInsteadOfLinqMethods.cs b/analyzers/its/sources/Net7/Net7/features/SetPropertiesInsteadOfLinqMethods.cs new file mode 100644 index 00000000000..8326d52d273 --- /dev/null +++ b/analyzers/its/sources/Net7/Net7/features/SetPropertiesInsteadOfLinqMethods.cs @@ -0,0 +1,14 @@ +using System.Linq; +using System.Collections.Generic; + +namespace Net7.features +{ + public static class SetPropertiesInsteadOfLinqMethods + { + static int Method() + { + var set = new SortedSet(); + return set.Min(); + } + } +} diff --git a/analyzers/rspec/cs/S6609.html b/analyzers/rspec/cs/S6609.html new file mode 100644 index 00000000000..6d86a9588c9 --- /dev/null +++ b/analyzers/rspec/cs/S6609.html @@ -0,0 +1,150 @@ +

Why is this an issue?

+

Both the Enumerable.Max extension method and the SortedSet<T>.Max property can be used to find the maximum value in +a SortedSet<T>. However, SortedSet<T>.Max is much faster than Enumerable.Max. For small +collections, the performance difference may be minor, but for large collections, it can be noticeable. The same applies for the Min +property as well.

+

Max and Min in SortedSet<T> exploit the fact that the set is implemented via a Red-Black +tree. The algorithm to find the Max/Min is "go left/right whenever possible". The operation has the time complexity +of O(h) which becomes O(ln(n)) due to the fact that the tree is balanced. This is much better than the O(n) +time complexity of extension methods.

+

Max and Min in ImmutableSortedSet<T> exploits a tree augmentation technique, storing the +Min, Max and Count values on each node of the data structure. The time complexity in this case is +O(1) that is significantly better than O(n) of extension methods.

+

Applies to:

+ +

What is the potential impact?

+

We measured a significant improvement both in execution time and memory allocation. For more details see the Benchmarks section from +the More info tab.

+

How to fix it

+

The Min and Max properties are defined on the following classes, and the extension method call can be replaced by calling +the propery instead:

+
    +
  • SortedSet<T>
  • +
  • ImmutableSortedSet<T>
  • +
  • ImmutableSortedSet<T>.Builder
  • +
+

Code examples

+

Noncompliant code example

+
+int GetMax(SortedSet<int> data) =>
+    data.Max();
+
+
+int GetMin(SortedSet<int> data) =>
+    data.Min();
+
+

Compliant solution

+
+int GetMax(SortedSet<int> data) =>
+    data.Max;
+
+
+int GetMin(SortedSet<int> data) =>
+    data.Min;
+
+

Resources

+

Documentation

+ +

Benchmarks

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodRuntimeMeanStdDevAllocated

MaxMethod

.NET 7.0

68,961.483 us

499.6623 us

248063 B

MaxProperty

.NET 7.0

4.638 us

0.0634 us

-

MaxMethod

.NET Framework 4.6.2

85,827.359 us

1,531.1611 us

281259 B

MaxProperty

.NET Framework 4.6.2

67.682 us

0.3757 us

312919 B

+

The results were generated by running the following snippet with BenchmarkDotNet:

+
+private SortedSet<string> data;
+
+[Params(1_000)]
+public int Iterations;
+
+[GlobalSetup]
+public void Setup() =>
+    data = new SortedSet<string>(Enumerable.Range(0, Iterations).Select(x => Guid.NewGuid().ToString()));
+
+[Benchmark(Baseline = true)]
+public void MaxMethod()
+{
+    for (var i = 0; i < Iterations; i++)
+    {
+        _ = data.Max();     // Max() extension method
+    }
+}
+
+[Benchmark]
+public void MaxProperty()
+{
+    for (var i = 0; i < Iterations; i++)
+    {
+        _ = data.Max;       // Max property
+    }
+}
+
+

Hardware configuration:

+
+BenchmarkDotNet=v0.13.5, OS=Windows 10 (10.0.19045.2846/22H2/2022Update)
+11th Gen Intel Core i7-11850H 2.50GHz, 1 CPU, 16 logical and 8 physical cores
+  [Host]               : .NET Framework 4.8 (4.8.4614.0), X64 RyuJIT VectorSize=256
+  .NET 7.0             : .NET 7.0.5 (7.0.523.17405), X64 RyuJIT AVX2
+  .NET Framework 4.6.2 : .NET Framework 4.8 (4.8.4614.0), X64 RyuJIT VectorSize=256
+
+ diff --git a/analyzers/rspec/cs/S6609.json b/analyzers/rspec/cs/S6609.json new file mode 100644 index 00000000000..c9137a4e699 --- /dev/null +++ b/analyzers/rspec/cs/S6609.json @@ -0,0 +1,17 @@ +{ + "title": "\"Min\/Max\" properties of \"Set\" types should be used instead of the \"Enumerable\" extension methods", + "type": "CODE_SMELL", + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "5min" + }, + "tags": [ + "performance" + ], + "defaultSeverity": "Minor", + "ruleSpecification": "RSPEC-6609", + "sqKey": "S6609", + "scope": "All", + "quickfix": "targeted" +} diff --git a/analyzers/rspec/cs/Sonar_way_profile.json b/analyzers/rspec/cs/Sonar_way_profile.json index 04bba1ef78a..b2d0ee25faa 100644 --- a/analyzers/rspec/cs/Sonar_way_profile.json +++ b/analyzers/rspec/cs/Sonar_way_profile.json @@ -276,6 +276,7 @@ "S6605", "S6607", "S6608", + "S6609", "S6610", "S6613" ] diff --git a/analyzers/rspec/vbnet/S6609.html b/analyzers/rspec/vbnet/S6609.html new file mode 100644 index 00000000000..8f8a58ae967 --- /dev/null +++ b/analyzers/rspec/vbnet/S6609.html @@ -0,0 +1,154 @@ +

Why is this an issue?

+

Both the Enumerable.Max extension method and the SortedSet<T>.Max property can be used to find the maximum value in +a SortedSet<T>. However, SortedSet<T>.Max is much faster than Enumerable.Max. For small +collections, the performance difference may be minor, but for large collections, it can be noticeable. The same applies for the Min +property as well.

+

Max and Min in SortedSet<T> exploit the fact that the set is implemented via a Red-Black +tree. The algorithm to find the Max/Min is "go left/right whenever possible". The operation has the time complexity +of O(h) which becomes O(ln(n)) due to the fact that the tree is balanced. This is much better than the O(n) +time complexity of extension methods.

+

Max and Min in ImmutableSortedSet<T> exploits a tree augmentation technique, storing the +Min, Max and Count values on each node of the data structure. The time complexity in this case is +O(1) that is significantly better than O(n) of extension methods.

+

Applies to:

+ +

What is the potential impact?

+

We measured a significant improvement both in execution time and memory allocation. For more details see the Benchmarks section from +the More info tab.

+

How to fix it

+

The Min and Max properties are defined on the following classes, and the extension method call can be replaced by calling +the propery instead:

+
    +
  • SortedSet<T>
  • +
  • ImmutableSortedSet<T>
  • +
  • ImmutableSortedSet<T>.Builder
  • +
+

Code examples

+

Noncompliant code example

+
+Function GetMax(data As SortedSet(Of Integer)) As Integer
+    Return Enumerable.Max(data)
+End Function
+
+
+Function GetMin(data As SortedSet(Of Integer)) As Integer
+    Return Enumerable.Min(data)
+End Function
+
+

Compliant solution

+
+Function GetMax(data As SortedSet(Of Integer)) As Integer
+    Return data.Max()
+End Function
+
+
+Function GetMin(data As SortedSet(Of Integer)) As Integer
+    Return data.Min()
+End Function
+
+

Resources

+

Documentation

+ +

Benchmarks

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodRuntimeMeanStdDevAllocated

MaxMethod

.NET 7.0

68,961.483 us

499.6623 us

248063 B

MaxProperty

.NET 7.0

4.638 us

0.0634 us

-

MaxMethod

.NET Framework 4.6.2

85,827.359 us

1,531.1611 us

281259 B

MaxProperty

.NET Framework 4.6.2

67.682 us

0.3757 us

312919 B

+

The results were generated by running the following snippet with BenchmarkDotNet:

+
+private SortedSet<string> data;
+
+[Params(1_000)]
+public int Iterations;
+
+[GlobalSetup]
+public void Setup() =>
+    data = new SortedSet<string>(Enumerable.Range(0, Iterations).Select(x => Guid.NewGuid().ToString()));
+
+[Benchmark(Baseline = true)]
+public void MaxMethod()
+{
+    for (var i = 0; i < Iterations; i++)
+    {
+        _ = data.Max();     // Max() extension method
+    }
+}
+
+[Benchmark]
+public void MaxProperty()
+{
+    for (var i = 0; i < Iterations; i++)
+    {
+        _ = data.Max;       // Max property
+    }
+}
+
+

Hardware configuration:

+
+BenchmarkDotNet=v0.13.5, OS=Windows 10 (10.0.19045.2846/22H2/2022Update)
+11th Gen Intel Core i7-11850H 2.50GHz, 1 CPU, 16 logical and 8 physical cores
+  [Host]               : .NET Framework 4.8 (4.8.4614.0), X64 RyuJIT VectorSize=256
+  .NET 7.0             : .NET 7.0.5 (7.0.523.17405), X64 RyuJIT AVX2
+  .NET Framework 4.6.2 : .NET Framework 4.8 (4.8.4614.0), X64 RyuJIT VectorSize=256
+
+ diff --git a/analyzers/rspec/vbnet/S6609.json b/analyzers/rspec/vbnet/S6609.json new file mode 100644 index 00000000000..c9137a4e699 --- /dev/null +++ b/analyzers/rspec/vbnet/S6609.json @@ -0,0 +1,17 @@ +{ + "title": "\"Min\/Max\" properties of \"Set\" types should be used instead of the \"Enumerable\" extension methods", + "type": "CODE_SMELL", + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "5min" + }, + "tags": [ + "performance" + ], + "defaultSeverity": "Minor", + "ruleSpecification": "RSPEC-6609", + "sqKey": "S6609", + "scope": "All", + "quickfix": "targeted" +} diff --git a/analyzers/rspec/vbnet/Sonar_way_profile.json b/analyzers/rspec/vbnet/Sonar_way_profile.json index a93b337d747..a3c85925828 100644 --- a/analyzers/rspec/vbnet/Sonar_way_profile.json +++ b/analyzers/rspec/vbnet/Sonar_way_profile.json @@ -134,6 +134,7 @@ "S6605", "S6607", "S6608", + "S6609", "S6610", "S6613" ] diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/SetPropertiesInsteadOfMethods.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/SetPropertiesInsteadOfMethods.cs new file mode 100644 index 00000000000..939bc8702e2 --- /dev/null +++ b/analyzers/src/SonarAnalyzer.CSharp/Rules/SetPropertiesInsteadOfMethods.cs @@ -0,0 +1,33 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2023 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 SetPropertiesInsteadOfMethods : SetPropertiesInsteadOfMethodsBase +{ + protected override ILanguageFacade Language => CSharpFacade.Instance; + + protected override bool HasCorrectArgumentCount(InvocationExpressionSyntax invocation) => + invocation.HasExactlyNArguments(0); + + protected override bool TryGetOperands(InvocationExpressionSyntax invocation, out SyntaxNode typeNode, out SyntaxNode methodNode) => + invocation.TryGetOperands(out typeNode, out methodNode); +} diff --git a/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs b/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs index 4937cc89e7e..9cf1b07e757 100644 --- a/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs +++ b/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs @@ -193,6 +193,7 @@ public sealed partial class KnownType public static readonly KnownType System_Collections_Generic_KeyValuePair_TKey_TValue = new("System.Collections.Generic.KeyValuePair", "TKey", "TValue"); public static readonly KnownType System_Collections_Generic_List_T = new("System.Collections.Generic.List", "T"); public static readonly KnownType System_Collections_Generic_Queue_T = new("System.Collections.Generic.Queue", "T"); + public static readonly KnownType System_Collections_Generic_SortedSet_T = new("System.Collections.Generic.SortedSet", "T"); public static readonly KnownType System_Collections_Generic_Stack_T = new("System.Collections.Generic.Stack", "T"); public static readonly KnownType System_Collections_Generic_LinkedList_T = new("System.Collections.Generic.LinkedList", "T"); public static readonly KnownType System_Collections_ICollection = new("System.Collections.ICollection"); diff --git a/analyzers/src/SonarAnalyzer.Common/Rules/SetPropertiesInsteadOfMethodsBase.cs b/analyzers/src/SonarAnalyzer.Common/Rules/SetPropertiesInsteadOfMethodsBase.cs new file mode 100644 index 00000000000..e558453751f --- /dev/null +++ b/analyzers/src/SonarAnalyzer.Common/Rules/SetPropertiesInsteadOfMethodsBase.cs @@ -0,0 +1,65 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2023 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; + +public abstract class SetPropertiesInsteadOfMethodsBase : SonarDiagnosticAnalyzer + where TSyntaxKind : struct + where TInvocation : SyntaxNode +{ + private const string DiagnosticId = "S6609"; + protected override string MessageFormat => "\"{0}\" property of Set type should be used instead of the \"{0}()\" extension method."; + + private static readonly ImmutableArray TargetTypes = ImmutableArray.Create( + KnownType.System_Collections_Generic_SortedSet_T, + KnownType.System_Collections_Immutable_ImmutableSortedSet_T); + + protected abstract bool HasCorrectArgumentCount(TInvocation invocation); + protected abstract bool TryGetOperands(TInvocation invocation, out SyntaxNode typeNode, out SyntaxNode methodNode); + + protected SetPropertiesInsteadOfMethodsBase() : base(DiagnosticId) { } + + protected override void Initialize(SonarAnalysisContext context) => + context.RegisterNodeAction(Language.GeneratedCodeRecognizer, c => + { + var invocation = (TInvocation)c.Node; + var methodName = Language.GetName(invocation); + + if (HasCorrectName(methodName) + && HasCorrectArgumentCount(invocation) + && TryGetOperands(invocation, out var typeNode, out var methodNode) + && IsCorrectType(typeNode, c.SemanticModel) + && IsCorrectCall(methodNode, c.SemanticModel)) + { + c.ReportIssue(Diagnostic.Create(Rule, Language.Syntax.NodeIdentifier(invocation)?.GetLocation(), methodName)); + } + }, Language.SyntaxKind.InvocationExpression); + + private bool HasCorrectName(string methodName) => + methodName.Equals(nameof(Enumerable.Min), Language.NameComparison) + || methodName.Equals(nameof(Enumerable.Max), Language.NameComparison); + + private static bool IsCorrectType(SyntaxNode node, SemanticModel model) => + model.GetTypeInfo(node).Type.DerivesFromAny(TargetTypes); + + private static bool IsCorrectCall(SyntaxNode node, SemanticModel model) => + model.GetSymbolInfo(node).Symbol is IMethodSymbol method + && method.IsExtensionOn(KnownType.System_Collections_Generic_IEnumerable_T); +} diff --git a/analyzers/src/SonarAnalyzer.VisualBasic/Rules/SetPropertiesInsteadOfMethods.cs b/analyzers/src/SonarAnalyzer.VisualBasic/Rules/SetPropertiesInsteadOfMethods.cs new file mode 100644 index 00000000000..90fe0970b07 --- /dev/null +++ b/analyzers/src/SonarAnalyzer.VisualBasic/Rules/SetPropertiesInsteadOfMethods.cs @@ -0,0 +1,38 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2023 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.VisualBasic; + +[DiagnosticAnalyzer(LanguageNames.VisualBasic)] +public sealed class SetPropertiesInsteadOfMethods : SetPropertiesInsteadOfMethodsBase +{ + protected override ILanguageFacade Language => VisualBasicFacade.Instance; + + protected override bool HasCorrectArgumentCount(InvocationExpressionSyntax invocation) => + invocation.HasExactlyNArguments(1); + + // In VB you need to be explicit: Enumerable.Min(set), because set.Min() resolves to the property. + protected override bool TryGetOperands(InvocationExpressionSyntax invocation, out SyntaxNode typeNode, out SyntaxNode methodNode) + { + typeNode = invocation.ArgumentList.Arguments[0].GetExpression(); + methodNode = invocation; + return true; + } +} diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/PackagingTests/RuleTypeMappingCS.cs b/analyzers/tests/SonarAnalyzer.UnitTest/PackagingTests/RuleTypeMappingCS.cs index e98f770d50e..0182b5f854c 100644 --- a/analyzers/tests/SonarAnalyzer.UnitTest/PackagingTests/RuleTypeMappingCS.cs +++ b/analyzers/tests/SonarAnalyzer.UnitTest/PackagingTests/RuleTypeMappingCS.cs @@ -6533,7 +6533,7 @@ internal static class RuleTypeMappingCS // ["S6606"], ["S6607"] = "CODE_SMELL", ["S6608"] = "CODE_SMELL", - // ["S6609"], + ["S6609"] = "CODE_SMELL", ["S6610"] = "CODE_SMELL", // ["S6611"], // ["S6612"], diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/PackagingTests/RuleTypeMappingVB.cs b/analyzers/tests/SonarAnalyzer.UnitTest/PackagingTests/RuleTypeMappingVB.cs index 78b53a22999..0649425bd54 100644 --- a/analyzers/tests/SonarAnalyzer.UnitTest/PackagingTests/RuleTypeMappingVB.cs +++ b/analyzers/tests/SonarAnalyzer.UnitTest/PackagingTests/RuleTypeMappingVB.cs @@ -6533,7 +6533,7 @@ internal static class RuleTypeMappingVB // ["S6606"], ["S6607"] = "CODE_SMELL", ["S6608"] = "CODE_SMELL", - // ["S6609"], + ["S6609"] = "CODE_SMELL", ["S6610"] = "CODE_SMELL", // ["S6611"], // ["S6612"], diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/Rules/SetPropertiesInsteadOfMethodsTest.cs b/analyzers/tests/SonarAnalyzer.UnitTest/Rules/SetPropertiesInsteadOfMethodsTest.cs new file mode 100644 index 00000000000..85bef63bb4a --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.UnitTest/Rules/SetPropertiesInsteadOfMethodsTest.cs @@ -0,0 +1,49 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2023 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. + */ + +using CS = SonarAnalyzer.Rules.CSharp; +using VB = SonarAnalyzer.Rules.VisualBasic; + +namespace SonarAnalyzer.UnitTest.Rules; + +[TestClass] +public class SetPropertiesInsteadOfMethodsTest +{ + private readonly VerifierBuilder builderCS = new VerifierBuilder(); + + [TestMethod] + public void SetPropertiesInsteadOfMethods_CS() => + builderCS.AddPaths("SetPropertiesInsteadOfMethods.cs").Verify(); + +#if NET + + [TestMethod] + public void SetPropertiesInsteadOfMethods_CS_Immutable() => + builderCS.AddPaths("SetPropertiesInsteadOfMethods.Immutable.cs") + .WithTopLevelStatements() + .AddReferences(MetadataReferenceFacade.SystemCollections) + .Verify(); + +#endif + + [TestMethod] + public void SetPropertiesInsteadOfMethods_VB() => + new VerifierBuilder().AddPaths("SetPropertiesInsteadOfMethods.vb").Verify(); +} diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/SetPropertiesInsteadOfMethods.Immutable.cs b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/SetPropertiesInsteadOfMethods.Immutable.cs new file mode 100644 index 00000000000..1e041b312b9 --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/SetPropertiesInsteadOfMethods.Immutable.cs @@ -0,0 +1,29 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Collections.Immutable; + +var sortedSet = ImmutableSortedSet.Create(); + +_ = sortedSet.Min(); // Noncompliant {{"Min" property of Set type should be used instead of the "Min()" extension method.}} +// ^^^ +_ = sortedSet?.Min(); // Noncompliant {{"Min" property of Set type should be used instead of the "Min()" extension method.}} +// ^^^ +_ = sortedSet.Max(); // Noncompliant {{"Max" property of Set type should be used instead of the "Max()" extension method.}} +// ^^^ +_ = sortedSet?.Max(); // Noncompliant {{"Max" property of Set type should be used instead of the "Max()" extension method.}} +// ^^^ + +Func, int> funcMin = x => x.Min(); // Noncompliant +Func, int> funcMax = x => x.Max(); // Noncompliant + +SortedSet DoWork() => null; + +DoWork().Min(); // Noncompliant +DoWork().Max(); // Noncompliant + +DoWork()?.Min(); // Noncompliant +DoWork()?.Max(); // Noncompliant + +ImmutableSortedSet.Create().Add(42).Min(); // Noncompliant +ImmutableSortedSet.CreateBuilder().ToImmutable().Max(); // Noncompliant diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/SetPropertiesInsteadOfMethods.cs b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/SetPropertiesInsteadOfMethods.cs new file mode 100644 index 00000000000..5c761be6dce --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/SetPropertiesInsteadOfMethods.cs @@ -0,0 +1,115 @@ +using System; +using System.Linq; +using System.Collections; +using System.Collections.Generic; + +static class Program +{ + static void SortedSet() + { + var sortedSet = new SortedSet(); + + _ = sortedSet.Min(); // Noncompliant {{"Min" property of Set type should be used instead of the "Min()" extension method.}} + // ^^^ + _ = sortedSet?.Min(); // Noncompliant {{"Min" property of Set type should be used instead of the "Min()" extension method.}} + // ^^^ + _ = sortedSet.Max(); // Noncompliant {{"Max" property of Set type should be used instead of the "Max()" extension method.}} + // ^^^ + _ = sortedSet?.Max(); // Noncompliant {{"Max" property of Set type should be used instead of the "Max()" extension method.}} + // ^^^ + + Func, int> funcMin = x => x.Min(); // Noncompliant + Func, int> funcMax = x => x.Max(); // Noncompliant + + SortedSet DoWork() => null; + + DoWork().Min(); // Noncompliant + DoWork().Max(); // Noncompliant + + DoWork()?.Min(); // Noncompliant + DoWork()?.Max(); // Noncompliant + + new SortedSet { 42 }.Min(); // Noncompliant + new SortedSet { 42 }.Max(); // Noncompliant + + sortedSet.Min(x => 42f); // Compliant, predicate used + sortedSet?.Max(x => x); // Compliant, predicate used + } + + static void DerivesFromSortedSet() + { + var sortedSetDerived = new DerivesFromSetType(); + + (true ? sortedSetDerived : sortedSetDerived).Min(); // Noncompliant + (true ? sortedSetDerived : sortedSetDerived).Max(); // Noncompliant + + (sortedSetDerived ?? sortedSetDerived).Min(); // Noncompliant + (sortedSetDerived ?? sortedSetDerived).Max(); // Noncompliant + + (sortedSetDerived ?? (true ? sortedSetDerived : sortedSetDerived)).Min(); // Noncompliant + (sortedSetDerived ?? (true ? sortedSetDerived : sortedSetDerived)).Max(); // Noncompliant + + sortedSetDerived.Fluent().Fluent().Fluent().Fluent().Min(); // Noncompliant + sortedSetDerived.Fluent().Fluent().Fluent().Fluent()?.Max(); // Noncompliant + sortedSetDerived.Fluent().Fluent().Fluent()?.Fluent().Min(); // Noncompliant + sortedSetDerived.Fluent().Fluent().Fluent()?.Fluent()?.Max(); // Noncompliant + sortedSetDerived.Fluent().Fluent()?.Fluent().Fluent().Min(); // Noncompliant + sortedSetDerived.Fluent().Fluent()?.Fluent().Fluent()?.Max(); // Noncompliant + sortedSetDerived.Fluent().Fluent()?.Fluent()?.Fluent().Min(); // Noncompliant + sortedSetDerived.Fluent().Fluent()?.Fluent()?.Fluent()?.Max(); // Noncompliant + sortedSetDerived.Fluent()?.Fluent().Fluent().Fluent().Min(); // Noncompliant + sortedSetDerived.Fluent()?.Fluent().Fluent().Fluent()?.Max(); // Noncompliant + sortedSetDerived.Fluent()?.Fluent().Fluent()?.Fluent().Min(); // Noncompliant + sortedSetDerived.Fluent()?.Fluent().Fluent()?.Fluent()?.Max(); // Noncompliant + sortedSetDerived.Fluent()?.Fluent()?.Fluent().Fluent().Min(); // Noncompliant + sortedSetDerived.Fluent()?.Fluent()?.Fluent().Fluent()?.Max(); // Noncompliant + sortedSetDerived.Fluent()?.Fluent()?.Fluent()?.Fluent().Min(); // Noncompliant + sortedSetDerived.Fluent()?.Fluent()?.Fluent()?.Fluent()?.Max(); // Noncompliant + // ^^^ + + sortedSetDerived.Min(x => x == 42f); // Compliant, comparer used + sortedSetDerived?.Max(x => x == 42); // Compliant, comparer used + } + + static void TrueNegatives() + { + var set = new SortedSet(); + _ = set.Min; // Compliant + _ = set.Max; // Compliant + + T Min() => default(T); + T Max() => default(T); + + Min(); // Compliant + Max(); // Compliant + + var doesNotDerive = new DoesNotDeriveFromSetType(); + doesNotDerive.Min(); // Compliant, does not derive from Set type + doesNotDerive.Max(); // Compliant, does not derive from Set type + + var hidden = new DerivesFromSetTypeButHidesEnumerableMethods(); + hidden.Min(); // Compliant, hides LINQ's Min extension + hidden.Max(); // Compliant, hides LINQ's Max extension + + dynamic dynamicSet = new SortedSet(); + dynamicSet.Min(); // Compliant, dynamic type + dynamicSet.Max(); // Compliant, dynamic type + } +} + +class DerivesFromSetType : SortedSet +{ + public DerivesFromSetType Fluent() => this; +} + +class DerivesFromSetTypeButHidesEnumerableMethods : SortedSet +{ + public T Min() => default(T); + public T Max() => default(T); +} + +class DoesNotDeriveFromSetType : IEnumerable +{ + public IEnumerator GetEnumerator() => null; + IEnumerator IEnumerable.GetEnumerator() => null; +} diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/SetPropertiesInsteadOfMethods.vb b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/SetPropertiesInsteadOfMethods.vb new file mode 100644 index 00000000000..496379b5e21 --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/SetPropertiesInsteadOfMethods.vb @@ -0,0 +1,90 @@ +Imports System +Imports System.Linq +Imports System.Collections +Imports System.Collections.Generic +Imports System.Collections.Immutable + +Module Program + Sub SortedSet() + Dim sortedSet = New SortedSet(Of Integer)() + + Dim dummy = Enumerable.Min(sortedSet) ' Noncompliant {{"Min" property of Set type should be used instead of the "Min()" extension method.}} + dummy = Enumerable.Max(sortedSet) ' Noncompliant {{"Max" property of Set type should be used instead of the "Max()" extension method.}} + + Dim funcMin As Func(Of SortedSet(Of Integer), Integer) = Function(x) Enumerable.Min(x) ' Noncompliant + Dim funcMax As Func(Of SortedSet(Of Integer), Integer) = Function(x) Enumerable.Max(x) ' Noncompliant + + Dim doWork As Func(Of SortedSet(Of Integer)) = Function() Nothing + + dummy = Enumerable.Min(doWork()) ' Noncompliant + dummy = Enumerable.Min(doWork.Invoke()) ' Noncompliant + + dummy = Enumerable.Min(New SortedSet(Of Integer)({42})) ' Noncompliant + dummy = Enumerable.Max(New SortedSet(Of Integer)({42})) ' Noncompliant + + dummy = Enumerable.Min(sortedSet, Function(x) 42) ' Compliant, predicate used + dummy = Enumerable.Max(sortedSet, Function(x) 42.0F) ' Compliant, predicate used + End Sub + + Sub DerivesFromSortedSet() + Dim sortedSetDerived = New DerivesFromSetType(Of Integer)() + + Dim ternary = IIf(True, sortedSetDerived, sortedSetDerived) + Enumerable.Min(ternary) ' Compliant - IIf returns un-typed object + Enumerable.Max(ternary) ' Compliant - IIf returns untyped object + + Enumerable.Min(sortedSetDerived.Fluent().Fluent().Fluent().Fluent()) ' Noncompliant + Enumerable.Max(sortedSetDerived.Fluent().Fluent().Fluent()?.Fluent()) ' Noncompliant + Enumerable.Min(sortedSetDerived.Fluent().Fluent()?.Fluent().Fluent()) ' Noncompliant + Enumerable.Max(sortedSetDerived.Fluent().Fluent()?.Fluent()?.Fluent()) ' Noncompliant + Enumerable.Min(sortedSetDerived.Fluent()?.Fluent().Fluent().Fluent()) ' Noncompliant + Enumerable.Max(sortedSetDerived.Fluent()?.Fluent().Fluent()?.Fluent()) ' Noncompliant + Enumerable.Min(sortedSetDerived.Fluent()?.Fluent()?.Fluent().Fluent()) ' Noncompliant + Enumerable.Max(sortedSetDerived.Fluent()?.Fluent()?.Fluent()?.Fluent()) ' Noncompliant + ' ^^^ + Enumerable.Min(sortedSetDerived, Function(x) x = 42) ' Compliant, predicate used + Enumerable.Max(sortedSetDerived, Function(x) x = 42.0F) ' Compliant, predicate used + End Sub + + Sub TrueNegatives() + Dim sortedSet = New SortedSet(Of Integer)() + Dim dummy = sortedSet.Min ' Compliant + dummy = sortedSet.Max ' Compliant + dummy = sortedSet.Min() ' Compliant + dummy = sortedSet.Max() ' Compliant + + Dim doesNotDerive = New DoesNotDeriveFromSetType(Of Integer)() + doesNotDerive.Min() ' Compliant, does not derive from Set type + doesNotDerive.Max() ' Compliant, does not derive from Set type + dummy = Enumerable.Min(doesNotDerive) ' Compliant, does not derive from Set type + dummy = Enumerable.Max(doesNotDerive) ' Compliant, does not derive from Set type + End Sub +End Module + +Class DerivesFromSetType(Of T) + Inherits SortedSet(Of T) + + Public Function Fluent() As DerivesFromSetType(Of T) + Return Me + End Function +End Class + +Class DoesNotDeriveFromSetType(Of T) + Implements IEnumerable(Of T) + + Public Function Min() As Integer + Return 42 + End Function + + Public Function Max() As Integer + Return 42 + End Function + + Public Function GetEnumerator() As IEnumerator(Of T) Implements IEnumerable(Of T).GetEnumerator + Return Nothing + End Function + + Private Function IEnumerable_GetEnumerator() As IEnumerator Implements IEnumerable.GetEnumerator + Return Nothing + End Function +End Class