forked from dotnet/efcore
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Suppress warnings on uninitialized DbSet properties
Closes dotnet#21608
- Loading branch information
Showing
4 changed files
with
261 additions
and
8 deletions.
There are no files selected for viewing
90 changes: 90 additions & 0 deletions
90
src/EFCore.Analyzers/UninitializedDbSetDiagnosticSuppressor.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using System; | ||
using System.Collections.Immutable; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
using Microsoft.CodeAnalysis.Diagnostics; | ||
|
||
namespace Microsoft.EntityFrameworkCore; | ||
|
||
[DiagnosticAnalyzer(LanguageNames.CSharp)] | ||
public class UninitializedDbSetDiagnosticSuppressor : DiagnosticSuppressor | ||
{ | ||
private static readonly SuppressionDescriptor _suppressUninitializedDbSetRule = new( | ||
id: "SPR1001", | ||
suppressedDiagnosticId: "CS8618", | ||
justification: "DbSet properties on DbContext subclasses are automatically populated by the DbContext constructor"); | ||
|
||
/// <inheritdoc /> | ||
public override ImmutableArray<SuppressionDescriptor> SupportedSuppressions => ImmutableArray.Create(_suppressUninitializedDbSetRule); | ||
|
||
/// <inheritdoc /> | ||
public override void ReportSuppressions(SuppressionAnalysisContext context) | ||
{ | ||
INamedTypeSymbol? dbSetTypeSymbol = null; | ||
INamedTypeSymbol? dbContextTypeSymbol = null; | ||
|
||
foreach (var diagnostic in context.ReportedDiagnostics) | ||
{ | ||
if (diagnostic.Id != "CS8618" || !diagnostic.Location.IsInSource) | ||
{ | ||
continue; | ||
} | ||
|
||
// We have an warning about an uninitialized non-nullable property. | ||
// Get the node, and make sure it's a property who's type syntactically contains DbSet (fast check before getting the | ||
// semantic model, which is heavier). | ||
var sourceTree = diagnostic.Location.SourceTree; | ||
var node = sourceTree?.GetRoot().FindNode(diagnostic.Location.SourceSpan); | ||
if (node is not PropertyDeclarationSyntax propertyDeclarationSyntax | ||
|| !propertyDeclarationSyntax.Type.ToString().Contains("DbSet")) | ||
{ | ||
continue; | ||
} | ||
|
||
// Get the semantic symbol and do some basic checks | ||
if (context.GetSemanticModel(sourceTree!).GetDeclaredSymbol(node) is not IPropertySymbol propertySymbol | ||
|| propertySymbol.IsStatic | ||
|| propertySymbol.IsReadOnly) | ||
{ | ||
continue; | ||
} | ||
|
||
if (dbSetTypeSymbol is null || dbContextTypeSymbol is null) | ||
{ | ||
dbSetTypeSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.EntityFrameworkCore.DbSet`1"); | ||
dbContextTypeSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.EntityFrameworkCore.DbContext"); | ||
|
||
if (dbSetTypeSymbol is null || dbContextTypeSymbol is null) | ||
{ | ||
return; | ||
} | ||
} | ||
|
||
// Check that the property is actually a DbSet<T>, and that its containing type inherits from DbContext | ||
if (propertySymbol.Type.OriginalDefinition.Equals(dbSetTypeSymbol, SymbolEqualityComparer.Default) | ||
&& InheritsFromDbContext(propertySymbol.ContainingType, dbContextTypeSymbol)) | ||
{ | ||
context.ReportSuppression(Suppression.Create(SupportedSuppressions[0], diagnostic)); | ||
} | ||
|
||
static bool InheritsFromDbContext(ITypeSymbol typeSymbol, ITypeSymbol baseTypeSymbol) | ||
{ | ||
var baseType = typeSymbol.BaseType; | ||
while (baseType is not null) | ||
{ | ||
if (baseType.Equals(baseTypeSymbol, SymbolEqualityComparer.Default)) | ||
{ | ||
return true; | ||
} | ||
|
||
baseType = baseType.BaseType; | ||
} | ||
|
||
return false; | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
154 changes: 154 additions & 0 deletions
154
test/EFCore.Analyzers.Tests/UninitializedDbSetDiagnosticSuppressorTest.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using System.Threading.Tasks; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.Diagnostics; | ||
using Microsoft.EntityFrameworkCore.TestUtilities; | ||
using Xunit; | ||
|
||
namespace Microsoft.EntityFrameworkCore; | ||
|
||
public class UninitializedDbSetDiagnosticSuppressorTest : DiagnosticAnalyzerTestBase | ||
{ | ||
[ConditionalFact] | ||
public async Task DbSet_property_on_DbContext_is_suppressed() | ||
{ | ||
var source = @" | ||
public class MyDbContext : Microsoft.EntityFrameworkCore.DbContext | ||
{ | ||
public Microsoft.EntityFrameworkCore.DbSet<Blog> Blogs { get; set; } | ||
} | ||
public class Blog | ||
{ | ||
public int Id { get; set; } | ||
}"; | ||
|
||
var diagnostic = Assert.Single(await GetDiagnosticsFullSourceAsync(source)); | ||
|
||
Assert.Equal("CS8618", diagnostic.Id); | ||
Assert.True(diagnostic.IsSuppressed); | ||
} | ||
|
||
[ConditionalFact] | ||
public async Task Non_public_DbSet_property_on_DbContext_is_suppressed() | ||
{ | ||
var source = @" | ||
public class MyDbContext : Microsoft.EntityFrameworkCore.DbContext | ||
{ | ||
private Microsoft.EntityFrameworkCore.DbSet<Blog> Blogs { get; set; } | ||
} | ||
public class Blog | ||
{ | ||
public int Id { get; set; } | ||
}"; | ||
|
||
var diagnostic = Assert.Single(await GetDiagnosticsFullSourceAsync(source)); | ||
|
||
Assert.Equal("CS8618", diagnostic.Id); | ||
Assert.True(diagnostic.IsSuppressed); | ||
} | ||
|
||
[ConditionalFact] | ||
public async Task DbSet_property_with_non_public_setter_on_DbContext_is_suppressed() | ||
{ | ||
var source = @" | ||
public class MyDbContext : Microsoft.EntityFrameworkCore.DbContext | ||
{ | ||
public Microsoft.EntityFrameworkCore.DbSet<Blog> Blogs { get; private set; } | ||
} | ||
public class Blog | ||
{ | ||
public int Id { get; set; } | ||
}"; | ||
|
||
var diagnostic = Assert.Single(await GetDiagnosticsFullSourceAsync(source)); | ||
|
||
Assert.Equal("CS8618", diagnostic.Id); | ||
Assert.True(diagnostic.IsSuppressed); | ||
} | ||
|
||
[ConditionalFact] | ||
public async Task DbSet_property_without_setter_on_DbContext_is_not_suppressed() | ||
{ | ||
var source = @" | ||
public class MyDbContext : Microsoft.EntityFrameworkCore.DbContext | ||
{ | ||
public Microsoft.EntityFrameworkCore.DbSet<Blog> Blogs { get; } | ||
} | ||
public class Blog | ||
{ | ||
public int Id { get; set; } | ||
}"; | ||
|
||
var diagnostic = Assert.Single(await GetDiagnosticsFullSourceAsync(source)); | ||
|
||
Assert.Equal("CS8618", diagnostic.Id); | ||
Assert.False(diagnostic.IsSuppressed); | ||
} | ||
|
||
[ConditionalFact] | ||
public async Task Static_DbSet_property_on_DbContext_is_not_suppressed() | ||
{ | ||
var source = @" | ||
public class MyDbContext : Microsoft.EntityFrameworkCore.DbContext | ||
{ | ||
public static Microsoft.EntityFrameworkCore.DbSet<Blog> Blogs { get; set; } | ||
} | ||
public class Blog | ||
{ | ||
public int Id { get; set; } | ||
}"; | ||
|
||
var diagnostic = Assert.Single(await GetDiagnosticsFullSourceAsync(source)); | ||
|
||
Assert.Equal("CS8618", diagnostic.Id); | ||
Assert.False(diagnostic.IsSuppressed); | ||
} | ||
|
||
[ConditionalFact] | ||
public async Task Non_DbSet_property_on_DbContext_is_not_suppressed() | ||
{ | ||
var source = @" | ||
public class MyDbContext : Microsoft.EntityFrameworkCore.DbContext | ||
{ | ||
public string Name { get; set; } | ||
}"; | ||
|
||
var diagnostic = Assert.Single(await GetDiagnosticsFullSourceAsync(source)); | ||
|
||
Assert.Equal("CS8618", diagnostic.Id); | ||
Assert.False(diagnostic.IsSuppressed); | ||
} | ||
|
||
[ConditionalFact] | ||
public async Task DbSet_property_on_non_DbContext_is_not_suppressed() | ||
{ | ||
var source = @" | ||
public class Foo | ||
{ | ||
public Microsoft.EntityFrameworkCore.DbSet<Blog> Blogs { get; set; } | ||
} | ||
public class Blog | ||
{ | ||
public int Id { get; set; } | ||
}"; | ||
|
||
var diagnostic = Assert.Single(await GetDiagnosticsFullSourceAsync(source)); | ||
|
||
Assert.Equal("CS8618", diagnostic.Id); | ||
Assert.False(diagnostic.IsSuppressed); | ||
} | ||
|
||
protected Task<Diagnostic[]> GetDiagnosticsFullSourceAsync(string source) | ||
=> base.GetDiagnosticsFullSourceAsync(source, analyzerDiagnosticsOnly: false); | ||
|
||
protected override DiagnosticAnalyzer CreateDiagnosticAnalyzer() | ||
=> new UninitializedDbSetDiagnosticSuppressor(); | ||
} |