diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/InsteadOfAny.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/InsteadOfAny.cs index 7c7ea764ef3..4632b66decb 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Rules/InsteadOfAny.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Rules/InsteadOfAny.cs @@ -25,6 +25,18 @@ public sealed class InsteadOfAny : InsteadOfAnyBase Language => CSharpFacade.Instance; + protected override HashSet ExitParentKinds { get; } = new() + { + SyntaxKind.MethodDeclaration, + SyntaxKind.ConstructorDeclaration, + SyntaxKind.DestructorDeclaration, + SyntaxKind.GetAccessorDeclaration, + SyntaxKind.SetAccessorDeclaration, + SyntaxKind.CompilationUnit, + SyntaxKindEx.LocalFunctionStatement, + SyntaxKindEx.InitAccessorDeclaration + }; + protected override bool IsSimpleEqualityCheck(InvocationExpressionSyntax node, SemanticModel model) => GetArgumentExpression(node, 0) is SimpleLambdaExpressionSyntax lambda && lambda.Parameter.Identifier.ValueText is var lambdaVariableName diff --git a/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs b/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs index 02738c43075..036f8392254 100644 --- a/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs +++ b/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs @@ -83,6 +83,7 @@ public sealed partial class KnownType public static readonly KnownType Microsoft_Azure_WebJobs_FunctionNameAttribute = new("Microsoft.Azure.WebJobs.FunctionNameAttribute"); public static readonly KnownType Microsoft_Data_Sqlite_SqliteCommand = new("Microsoft.Data.Sqlite.SqliteCommand"); public static readonly KnownType Microsoft_EntityFrameworkCore_DbContextOptionsBuilder = new("Microsoft.EntityFrameworkCore.DbContextOptionsBuilder"); + public static readonly KnownType Microsoft_EntityFrameworkCore_DbSet_TEntity = new("Microsoft.EntityFrameworkCore.DbSet", "TEntity"); public static readonly KnownType Microsoft_EntityFrameworkCore_Migrations_Migration = new("Microsoft.EntityFrameworkCore.Migrations.Migration"); public static readonly KnownType Microsoft_EntityFrameworkCore_MySQLDbContextOptionsExtensions = new("Microsoft.EntityFrameworkCore.MySQLDbContextOptionsExtensions"); public static readonly KnownType Microsoft_EntityFrameworkCore_NpgsqlDbContextOptionsExtensions = new("Microsoft.EntityFrameworkCore.NpgsqlDbContextOptionsExtensions"); @@ -457,7 +458,7 @@ public sealed partial class KnownType public static readonly KnownType System_StackOverflowException = new("System.StackOverflowException"); public static readonly KnownType System_STAThreadAttribute = new("System.STAThreadAttribute"); public static readonly KnownType System_String = new("System.String"); - public static readonly KnownType System_String_Array = new("System.String") { IsArray = true}; + public static readonly KnownType System_String_Array = new("System.String") { IsArray = true }; public static readonly KnownType System_StringComparison = new("System.StringComparison"); public static readonly KnownType System_SystemException = new("System.SystemException"); public static readonly KnownType System_Text_RegularExpressions_Regex = new("System.Text.RegularExpressions.Regex"); diff --git a/analyzers/src/SonarAnalyzer.Common/Rules/InsteadOfAnyBase.cs b/analyzers/src/SonarAnalyzer.Common/Rules/InsteadOfAnyBase.cs index 787f96ebc54..f6dce5dc16d 100644 --- a/analyzers/src/SonarAnalyzer.Common/Rules/InsteadOfAnyBase.cs +++ b/analyzers/src/SonarAnalyzer.Common/Rules/InsteadOfAnyBase.cs @@ -39,7 +39,12 @@ public abstract class InsteadOfAnyBase : Son KnownType.System_Collections_Generic_HashSet_T, KnownType.System_Collections_Generic_SortedSet_T); + protected static readonly ImmutableArray DbSetTypes = ImmutableArray.Create( + KnownType.System_Data_Entity_DbSet_TEntity, + KnownType.Microsoft_EntityFrameworkCore_DbSet_TEntity); + protected abstract ILanguageFacade Language { get; } + protected abstract HashSet ExitParentKinds { get; } public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(existsRule, containsRule); @@ -62,7 +67,8 @@ protected override void Initialize(SonarAnalysisContext context) => && Language.Syntax.HasExactlyNArguments(invocation, 1) && Language.Syntax.TryGetOperands(invocation, out var left, out var right) && IsCorrectCall(right, c.SemanticModel) - && c.SemanticModel.GetTypeInfo(left).Type is { } type) + && c.SemanticModel.GetTypeInfo(left).Type is { } type + && !IsUsedByEntityFramework(invocation, c.SemanticModel)) { if (ExistsTypes.Any(x => type.DerivesFrom(x))) { @@ -124,4 +130,23 @@ private static bool IsCorrectCall(SyntaxNode right, SemanticModel model) => private void RaiseIssue(SonarSyntaxNodeReportingContext c, SyntaxNode invocation, DiagnosticDescriptor rule, string methodName) => c.ReportIssue(Diagnostic.Create(rule, Language.Syntax.NodeIdentifier(invocation)?.GetLocation(), methodName)); + + // See https://github.com/SonarSource/sonar-dotnet/issues/7286 + private bool IsUsedByEntityFramework(SyntaxNode node, SemanticModel model) + { + do + { + node = node.Parent; + + if (Language.Syntax.IsKind(node, Language.SyntaxKind.InvocationExpression) + && Language.Syntax.TryGetOperands(node, out var left, out var _) + && model.GetTypeInfo(left).Type.DerivesFromAny(DbSetTypes)) + { + return true; + } + } + while (!Language.Syntax.IsAnyKind(node, ExitParentKinds)); + + return false; + } } diff --git a/analyzers/src/SonarAnalyzer.VisualBasic/Rules/InsteadOfAny.cs b/analyzers/src/SonarAnalyzer.VisualBasic/Rules/InsteadOfAny.cs index 3dc24636d17..b61ad18ee43 100644 --- a/analyzers/src/SonarAnalyzer.VisualBasic/Rules/InsteadOfAny.cs +++ b/analyzers/src/SonarAnalyzer.VisualBasic/Rules/InsteadOfAny.cs @@ -25,6 +25,16 @@ public sealed class InsteadOfAny : InsteadOfAnyBase Language => VisualBasicFacade.Instance; + protected override HashSet ExitParentKinds { get; } = new() + { + SyntaxKind.SubStatement, + SyntaxKind.SubNewStatement, + SyntaxKind.FunctionStatement, + SyntaxKind.GetAccessorStatement, + SyntaxKind.SetAccessorStatement, + SyntaxKind.CompilationUnit, + }; + protected override bool IsSimpleEqualityCheck(InvocationExpressionSyntax node, SemanticModel model) => GetArgumentExpression(node, 0) is SingleLineLambdaExpressionSyntax lambda && lambda.SubOrFunctionHeader.ParameterList.Parameters is { Count: 1 } parameters diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/Rules/InsteadOfAnyTest.cs b/analyzers/tests/SonarAnalyzer.UnitTest/Rules/InsteadOfAnyTest.cs index 3bf68b9077a..5a1abd3d688 100644 --- a/analyzers/tests/SonarAnalyzer.UnitTest/Rules/InsteadOfAnyTest.cs +++ b/analyzers/tests/SonarAnalyzer.UnitTest/Rules/InsteadOfAnyTest.cs @@ -27,6 +27,7 @@ namespace SonarAnalyzer.UnitTest.Rules; public class InsteadOfAnyTest { private readonly VerifierBuilder builderCS = new VerifierBuilder(); + private readonly VerifierBuilder builderVB = new VerifierBuilder(); [TestMethod] public void InsteadOfAny_CS() => @@ -40,22 +41,43 @@ public void InsteadOfAny_TopLevelStatements() => .WithTopLevelStatements() .AddReferences(MetadataReferenceFacade.SystemCollections) .Verify(); +#endif [TestMethod] public void InsteadOfAny_EntityFramework() => - builderCS.AddPaths("InsteadOfAny.EntityFramework.cs") + builderCS.AddPaths("InsteadOfAny.EntityFramework.Core.cs") .WithOptions(ParseOptionsHelper.FromCSharp8) - .AddReferences(GetReferencesEntityFrameworkNetCore("2.2.6").Concat(NuGetMetadataReference.SystemComponentModelTypeConverter())) + .AddReferences(GetReferencesEntityFrameworkNetCore()) .Verify(); - internal static IEnumerable GetReferencesEntityFrameworkNetCore(string entityFrameworkVersion) => - Enumerable.Empty() - .Concat(NuGetMetadataReference.MicrosoftEntityFrameworkCore(entityFrameworkVersion)) - .Concat(NuGetMetadataReference.MicrosoftEntityFrameworkCoreRelational(entityFrameworkVersion)); +#if NETFRAMEWORK + + [TestMethod] + public void InsteadOfAny_EntityFramework_Framework() => + builderCS.AddPaths("InsteadOfAny.EntityFramework.Framework.cs") + .AddReferences(GetReferencesEntityFrameworkNetFramework()) + .Verify(); #endif [TestMethod] public void ExistsInsteadOfAny_VB() => - new VerifierBuilder().AddPaths("InsteadOfAny.vb").Verify(); + builderVB.AddPaths("InsteadOfAny.vb").Verify(); + + [TestMethod] + public void InsteadOfAny_EntityFramework_VB() => + builderVB.AddPaths("InsteadOfAny.EntityFramework.Core.vb") + .AddReferences(GetReferencesEntityFrameworkNetCore()) + .Verify(); + + private static IEnumerable GetReferencesEntityFrameworkNetCore() => + Enumerable.Empty() + .Concat(NuGetMetadataReference.MicrosoftEntityFrameworkCore("2.2.6")) + .Concat(NuGetMetadataReference.MicrosoftEntityFrameworkCoreRelational("2.2.6")) + .Concat(NuGetMetadataReference.SystemComponentModelTypeConverter()); + + private static IEnumerable GetReferencesEntityFrameworkNetFramework() => + Enumerable.Empty() + .Concat(NuGetMetadataReference.EntityFramework()); + } diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/InsteadOfAny.EntityFramework.cs b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/InsteadOfAny.EntityFramework.Core.cs similarity index 56% rename from analyzers/tests/SonarAnalyzer.UnitTest/TestCases/InsteadOfAny.EntityFramework.cs rename to analyzers/tests/SonarAnalyzer.UnitTest/TestCases/InsteadOfAny.EntityFramework.Core.cs index cef683c3586..a7c32094281 100644 --- a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/InsteadOfAny.EntityFramework.cs +++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/InsteadOfAny.EntityFramework.Core.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; // https://github.com/SonarSource/sonar-dotnet/issues/7286 @@ -10,19 +11,32 @@ public class MyEntity public int Id { get; set; } } - public class MyDbContext : DbContext + public class FirstContext: DbContext { public DbSet MyEntities { get; set; } } - public void GetEntities(MyDbContext dbContext, List ids) + public class SecondContext: DbContext { - _ = dbContext.MyEntities.Where(e => ids.Any(i => e.Id == i)); // Noncompliant - FP, should raise in context of EntityFramework queries. Exist cannot be translated to SQL query + public DbSet SecondEntities { get; set; } + } + + + public void GetEntities(FirstContext dbContext, List ids) + { + _ = dbContext.MyEntities.Where(e => ids.Any(i => e.Id == i)); // Compliant + _ = dbContext.MyEntities.Where(e => ids.Any(i => e.Equals(i))); // Compliant + _ = dbContext.MyEntities.Where(e => ids.Any(i => e.Id > i)); // Compliant + _ = dbContext.MyEntities.Where(e => ids.Any(i => e.Id is i)); // Error [CS0150] - // Noncompliant@+1 _ = dbContext.MyEntities.Where(e => ids.Any(i => e.Id is 2)); // Error [CS8122] - _ = dbContext.MyEntities.Where(e => ids.Any(i => e.Equals(i))); // Noncompliant - FP, should raise in context of EntityFramework queries. Exist cannot be translated to SQL query - // This will generate a runtime error as EF does not know how to translate it to SQL Query - _ = dbContext.MyEntities.Where(e => ids.Any(i => e.Id > i)); // Noncompliant - FP + } + + public async Task GetEntitiesAsync(SecondContext secondContext, List ids) + { + _ = await secondContext + .SecondEntities + .Where(e => ids.Any(i => e.Id == i)) + .ToListAsync(); } } diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/InsteadOfAny.EntityFramework.Core.vb b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/InsteadOfAny.EntityFramework.Core.vb new file mode 100644 index 00000000000..05a0cbc91cb --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/InsteadOfAny.EntityFramework.Core.vb @@ -0,0 +1,22 @@ +Imports System.Collections.Generic +Imports System.Linq +Imports Microsoft.EntityFrameworkCore + +Public Class EntityFrameworkTestcases + Public Class MyEntity2 + Public Property Id As Integer + End Class + + Public Class MyDbContext + Inherits DbContext + + Public Property MyEntities As DbSet(Of MyEntity2) + + Public Sub GetEntities(ByVal dbContext As MyDbContext, ByVal ids As List(Of Integer)) + Dim __ As IQueryable(Of MyEntity2) = dbContext.MyEntities.Where(Function(e) ids.Any(Function(i) e.Id = i)) ' Compliant + __ = dbContext.MyEntities.Where(Function(e) ids.Any(Function(i) e.Equals(i))) ' Compliant + __ = dbContext.MyEntities.Where(Function(e) ids.Any(Function(i) e.Id > i)) ' Compliant + __ = dbContext.MyEntities.Where(Function(e) ids.Any(Function(i) TypeOf e.Id Is i)) ' Error [BC30002] + End Sub + End Class +End Class diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/InsteadOfAny.EntityFramework.Framework.cs b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/InsteadOfAny.EntityFramework.Framework.cs new file mode 100644 index 00000000000..252c4e3cdce --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/InsteadOfAny.EntityFramework.Framework.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Remoting.Contexts; +using System.Threading.Tasks; +using System.Data.Entity; + +// https://github.com/SonarSource/sonar-dotnet/issues/7286 +public class EntityFrameworkReproGH7286 +{ + public class MyEntity + { + public int Id { get; set; } + } + + public class FirstContext : DbContext + { + public DbSet MyEntities { get; set; } + } + + public class SecondContext : DbContext + { + public DbSet SecondEntities { get; set; } + } + + + public void GetEntities(FirstContext dbContext, List ids) + { + _ = dbContext.MyEntities.Where(e => ids.Any(i => e.Id == i)); // Compliant + _ = dbContext.MyEntities.Where(e => ids.Any(i => e.Equals(i))); // Compliant + _ = dbContext.MyEntities.Where(e => ids.Any(i => e.Id > i)); // Compliant + } + + public async Task GetEntitiesAsync(SecondContext secondContext, List ids) + { + _ = await secondContext + .SecondEntities + .Where(e => ids.Any(i => e.Id == i)) + .ToListAsync(); + } +}