Skip to content

Commit 01f75b8

Browse files
Fix S6966 FP: EntityFrameworks IDbContextFactory CreateDbContext method is preferred over its Async counterpart (#9605)
1 parent 39b591d commit 01f75b8

File tree

3 files changed

+40
-8
lines changed

3 files changed

+40
-8
lines changed

analyzers/src/SonarAnalyzer.CSharp/Rules/UseAwaitableMethod.cs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ private static ImmutableArray<Func<IMethodSymbol, bool>> BuildExclusions(Compila
9595
{
9696
exclusions.Add(x => x.IsAny(KnownType.Microsoft_EntityFrameworkCore_DbSet_TEntity, ExcludedMethodNames)); // https://github.com/SonarSource/sonar-dotnet/issues/9269
9797
exclusions.Add(x => x.IsAny(KnownType.Microsoft_EntityFrameworkCore_DbContext, ExcludedMethodNames)); // https://github.com/SonarSource/sonar-dotnet/issues/9269
98+
// https://github.com/SonarSource/sonar-dotnet/issues/9590
99+
exclusions.Add(x => x.IsImplementingInterfaceMember(KnownType.Microsoft_EntityFrameworkCore_IDbContextFactory_TContext, "CreateDbContext"));
98100
}
99101
if (compilation.GetTypeByMetadataName(KnownType.FluentValidation_IValidator) is not null)
100102
{
@@ -109,25 +111,25 @@ private static ImmutableArray<Func<IMethodSymbol, bool>> BuildExclusions(Compila
109111
}
110112

111113
private static ImmutableArray<ISymbol> FindAwaitableAlternatives(WellKnownExtensionMethodContainer wellKnownExtensionMethodContainer, ImmutableArray<Func<IMethodSymbol, bool>> exclusions,
112-
InvocationExpressionSyntax invocationExpression, SemanticModel semanticModel, ISymbol containingSymbol, CancellationToken cancel)
114+
InvocationExpressionSyntax invocationExpression, SemanticModel model, ISymbol containingSymbol, CancellationToken cancel)
113115
{
114116
var awaitableRoot = GetAwaitableRootOfInvocation(invocationExpression);
115117
if (awaitableRoot is not { Parent: AwaitExpressionSyntax } // Invocation result is already awaited.
116118
&& invocationExpression.EnclosingScope() is { } scope
117119
&& IsAsyncCodeBlock(scope)
118-
&& semanticModel.GetSymbolInfo(invocationExpression, cancel).Symbol is IMethodSymbol { MethodKind: not MethodKind.DelegateInvoke } methodSymbol
120+
&& model.GetSymbolInfo(invocationExpression, cancel).Symbol is IMethodSymbol { MethodKind: not MethodKind.DelegateInvoke } methodSymbol
119121
&& !(methodSymbol.IsAwaitableNonDynamic() // The invoked method returns something awaitable (but it isn't awaited).
120122
|| methodSymbol.ContainingType.DerivesFromAny(ExcludedTypes))
121123
&& !exclusions.Any(x => x(methodSymbol)))
122124
{
123125
// Perf: Before doing (expensive) speculative re-binding in SpeculativeBindCandidates, we check if there is an "..Async()" alternative in scope.
124-
var invokedType = invocationExpression.Expression.GetLeftOfDot() is { } expression && semanticModel.GetTypeInfo(expression) is { Type: { } type }
126+
var invokedType = invocationExpression.Expression.GetLeftOfDot() is { } expression && model.GetTypeInfo(expression) is { Type: { } type }
125127
? type // A dotted expression: Lookup the type, left of the dot (this may be different from methodSymbol.ContainingType)
126128
: containingSymbol.ContainingType; // If not dotted, than the scope is the current type. Local function support is missing here.
127129
var members = GetMethodSymbolsInScope($"{methodSymbol.Name}Async", wellKnownExtensionMethodContainer, invokedType, methodSymbol.ContainingType);
128130
var awaitableCandidates = members.Where(x => x.IsAwaitableNonDynamic());
129131
// Get the method alternatives and exclude candidates that would resolve to the containing method (endless loop)
130-
var awaitableAlternatives = SpeculativeBindCandidates(semanticModel, awaitableRoot, invocationExpression, awaitableCandidates)
132+
var awaitableAlternatives = SpeculativeBindCandidates(model, awaitableRoot, invocationExpression, awaitableCandidates)
131133
.Where(x => !containingSymbol.Equals(x))
132134
.ToImmutableArray();
133135
return awaitableAlternatives;
@@ -148,15 +150,15 @@ private static IEnumerable<INamedTypeSymbol> WellKnownExtensionMethodContainer(W
148150
? extensionMethodContainer
149151
: [];
150152

151-
private static IEnumerable<ISymbol> SpeculativeBindCandidates(SemanticModel semanticModel, SyntaxNode awaitableRoot,
153+
private static IEnumerable<ISymbol> SpeculativeBindCandidates(SemanticModel model, SyntaxNode awaitableRoot,
152154
InvocationExpressionSyntax invocationExpression, IEnumerable<IMethodSymbol> awaitableCandidates) =>
153155
awaitableCandidates
154156
.Select(x => x.Name)
155157
.Distinct()
156-
.Select(x => SpeculativeBindCandidate(semanticModel, x, awaitableRoot, invocationExpression))
158+
.Select(x => SpeculativeBindCandidate(model, x, awaitableRoot, invocationExpression))
157159
.WhereNotNull();
158160

159-
private static IMethodSymbol SpeculativeBindCandidate(SemanticModel semanticModel, string candidateName, SyntaxNode awaitableRoot,
161+
private static IMethodSymbol SpeculativeBindCandidate(SemanticModel model, string candidateName, SyntaxNode awaitableRoot,
160162
InvocationExpressionSyntax invocationExpression)
161163
{
162164
var invocationIdentifierName = invocationExpression.GetMethodCallIdentifier()?.Parent;
@@ -165,7 +167,7 @@ private static IMethodSymbol SpeculativeBindCandidate(SemanticModel semanticMode
165167
return null;
166168
}
167169
var invocationReplaced = ReplaceInvocation(awaitableRoot, invocationExpression, invocationIdentifierName, candidateName);
168-
var speculativeSymbolInfo = semanticModel.GetSpeculativeSymbolInfo(invocationReplaced.SpanStart, invocationReplaced, SpeculativeBindingOption.BindAsExpression);
170+
var speculativeSymbolInfo = model.GetSpeculativeSymbolInfo(invocationReplaced.SpanStart, invocationReplaced, SpeculativeBindingOption.BindAsExpression);
169171
var speculativeSymbol = speculativeSymbolInfo.Symbol as IMethodSymbol;
170172
return speculativeSymbol;
171173
}

analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ public sealed partial class KnownType
136136
public static readonly KnownType Microsoft_EntityFrameworkCore_DbContextOptionsBuilder = new("Microsoft.EntityFrameworkCore.DbContextOptionsBuilder");
137137
public static readonly KnownType Microsoft_EntityFrameworkCore_DbSet_TEntity = new("Microsoft.EntityFrameworkCore.DbSet", "TEntity");
138138
public static readonly KnownType Microsoft_EntityFrameworkCore_EntityFrameworkQueryableExtensions = new("Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions");
139+
public static readonly KnownType Microsoft_EntityFrameworkCore_IDbContextFactory_TContext = new("Microsoft.EntityFrameworkCore.IDbContextFactory", "TContext");
139140
public static readonly KnownType Microsoft_EntityFrameworkCore_Migrations_Migration = new("Microsoft.EntityFrameworkCore.Migrations.Migration");
140141
public static readonly KnownType Microsoft_EntityFrameworkCore_MySQLDbContextOptionsExtensions = new("Microsoft.EntityFrameworkCore.MySQLDbContextOptionsExtensions");
141142
public static readonly KnownType Microsoft_EntityFrameworkCore_NpgsqlDbContextOptionsExtensions = new("Microsoft.EntityFrameworkCore.NpgsqlDbContextOptionsExtensions");

analyzers/tests/SonarAnalyzer.Test/TestCases/UseAwaitableMethod_EF.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,32 @@ public async Task DatabaseFacade(DatabaseFacade databaseFacade)
7979
databaseFacade.UseTransaction(null); // Noncompliant
8080
}
8181
}
82+
83+
// https://github.com/SonarSource/sonar-dotnet/issues/9590
84+
public class Repro_9590
85+
{
86+
public async Task DoSomeWork(IDbContextFactory<AppDbContext> factory, MyDbContextFactory factory2, NotADbContextFactory factory3)
87+
{
88+
using AppDbContext dbContext = factory.CreateDbContext(); // Compliant - CreateDbContextAsync is excluded
89+
using AppDbContext dbContext2 = factory2.CreateDbContext(); // Compliant - CreateDbContextAsync is excluded
90+
using AppDbContext dbContext3 = factory3.CreateDbContext(); // Noncompliant
91+
}
92+
93+
public class MyDbContextFactory : IDbContextFactory<AppDbContext>
94+
{
95+
public AppDbContext CreateDbContext() => throw new NotImplementedException();
96+
97+
public Task<AppDbContext> CreateDbContextAsync() => throw new NotImplementedException();
98+
}
99+
100+
public class NotADbContextFactory
101+
{
102+
public AppDbContext CreateDbContext() => throw new NotImplementedException();
103+
104+
public Task<AppDbContext> CreateDbContextAsync() => throw new NotImplementedException();
105+
}
106+
107+
public class AppDbContext : DbContext
108+
{
109+
}
110+
}

0 commit comments

Comments
 (0)