diff --git a/TUnit.Analyzers.Tests/DisposableFieldPropertyAnalyzerTests.cs b/TUnit.Analyzers.Tests/DisposableFieldPropertyAnalyzerTests.cs index 54e85764fa..4b545d6757 100644 --- a/TUnit.Analyzers.Tests/DisposableFieldPropertyAnalyzerTests.cs +++ b/TUnit.Analyzers.Tests/DisposableFieldPropertyAnalyzerTests.cs @@ -582,4 +582,619 @@ public async Task RecordPaymentUsingMappedCommand(CancellationToken cancellation """ ); } + + [Test] + public async Task New_Disposable_In_AsyncInitializer_Flags_Issue() + { + await Verifier + .VerifyAnalyzerAsync( + """ + using System; + using System.Net.Http; + using System.Threading.Tasks; + using TUnit.Core; + using TUnit.Core.Interfaces; + + public class DisposableFieldTests : IAsyncInitializer + { + private HttpClient? {|#0:_httpClient|}; + + public Task InitializeAsync() + { + _httpClient = new HttpClient(); + return Task.CompletedTask; + } + + [Test] + public void Test1() + { + } + } + """, + + Verifier.Diagnostic(Rules.Dispose_Member_In_Cleanup) + .WithLocation(0) + .WithArguments("_httpClient") + ); + } + + [Test] + public async Task New_Disposable_In_AsyncInitializer_No_Issue_When_Cleaned_Up() + { + await Verifier + .VerifyAnalyzerAsync( + """ + using System; + using System.Net.Http; + using System.Threading.Tasks; + using TUnit.Core; + using TUnit.Core.Interfaces; + + public class DisposableFieldTests : IAsyncInitializer, IAsyncDisposable + { + private HttpClient? _httpClient; + + public Task InitializeAsync() + { + _httpClient = new HttpClient(); + return Task.CompletedTask; + } + + public ValueTask DisposeAsync() + { + _httpClient?.Dispose(); + return ValueTask.CompletedTask; + } + + [Test] + public void Test1() + { + } + } + """ + ); + } + + // ======================================== + // FIELD INITIALIZATION TESTS + // ======================================== + + [Test] + public async Task FieldInitialization_Flags_Issue() + { + await Verifier + .VerifyAnalyzerAsync( + """ + using System.Net.Http; + using TUnit.Core; + + public class DisposableFieldTests + { + private HttpClient? {|#0:_httpClient|} = new HttpClient(); + + [Test] + public void Test1() + { + } + } + """, + + Verifier.Diagnostic(Rules.Dispose_Member_In_Cleanup) + .WithLocation(0) + .WithArguments("_httpClient") + ); + } + + [Test] + public async Task FieldInitialization_No_Issue_When_Disposed_In_Dispose() + { + await Verifier + .VerifyAnalyzerAsync( + """ + using System; + using System.Net.Http; + using TUnit.Core; + + public class DisposableFieldTests : IDisposable + { + private HttpClient? _httpClient = new HttpClient(); + + public void Dispose() + { + _httpClient?.Dispose(); + } + + [Test] + public void Test1() + { + } + } + """ + ); + } + + [Test] + public async Task FieldInitialization_No_Issue_When_Disposed_In_DisposeAsync() + { + await Verifier + .VerifyAnalyzerAsync( + """ + using System; + using System.Net.Http; + using System.Threading.Tasks; + using TUnit.Core; + + public class DisposableFieldTests : IAsyncDisposable + { + private HttpClient? _httpClient = new HttpClient(); + + public ValueTask DisposeAsync() + { + _httpClient?.Dispose(); + return ValueTask.CompletedTask; + } + + [Test] + public void Test1() + { + } + } + """ + ); + } + + [Test] + public async Task FieldInitialization_No_Issue_When_Disposed_In_After() + { + await Verifier + .VerifyAnalyzerAsync( + """ + using System.Net.Http; + using TUnit.Core; + + public class DisposableFieldTests + { + private HttpClient? _httpClient = new HttpClient(); + + [After(HookType.Test)] + public void Cleanup() + { + _httpClient?.Dispose(); + } + + [Test] + public void Test1() + { + } + } + """ + ); + } + + // ======================================== + // CONSTRUCTOR INITIALIZATION TESTS + // ======================================== + + [Test] + public async Task Constructor_Flags_Issue() + { + await Verifier + .VerifyAnalyzerAsync( + """ + using System.Net.Http; + using TUnit.Core; + + public class DisposableFieldTests + { + private HttpClient? {|#0:_httpClient|}; + + public DisposableFieldTests() + { + _httpClient = new HttpClient(); + } + + [Test] + public void Test1() + { + } + } + """, + + Verifier.Diagnostic(Rules.Dispose_Member_In_Cleanup) + .WithLocation(0) + .WithArguments("_httpClient") + ); + } + + [Test] + public async Task Constructor_No_Issue_When_Disposed_In_Dispose() + { + await Verifier + .VerifyAnalyzerAsync( + """ + using System; + using System.Net.Http; + using TUnit.Core; + + public class DisposableFieldTests : IDisposable + { + private HttpClient? _httpClient; + + public DisposableFieldTests() + { + _httpClient = new HttpClient(); + } + + public void Dispose() + { + _httpClient?.Dispose(); + } + + [Test] + public void Test1() + { + } + } + """ + ); + } + + [Test] + public async Task Constructor_No_Issue_When_Disposed_In_DisposeAsync() + { + await Verifier + .VerifyAnalyzerAsync( + """ + using System; + using System.Net.Http; + using System.Threading.Tasks; + using TUnit.Core; + + public class DisposableFieldTests : IAsyncDisposable + { + private HttpClient? _httpClient; + + public DisposableFieldTests() + { + _httpClient = new HttpClient(); + } + + public ValueTask DisposeAsync() + { + _httpClient?.Dispose(); + return ValueTask.CompletedTask; + } + + [Test] + public void Test1() + { + } + } + """ + ); + } + + [Test] + public async Task Constructor_No_Issue_When_Disposed_In_After() + { + await Verifier + .VerifyAnalyzerAsync( + """ + using System.Net.Http; + using TUnit.Core; + + public class DisposableFieldTests + { + private HttpClient? _httpClient; + + public DisposableFieldTests() + { + _httpClient = new HttpClient(); + } + + [After(HookType.Test)] + public void Cleanup() + { + _httpClient?.Dispose(); + } + + [Test] + public void Test1() + { + } + } + """ + ); + } + + // ======================================== + // BEFORE(TEST) WITH DISPOSE/DISPOSEASYNC TESTS + // ======================================== + + [Test] + public async Task BeforeTest_No_Issue_When_Disposed_In_Dispose() + { + await Verifier + .VerifyAnalyzerAsync( + """ + using System; + using System.Net.Http; + using TUnit.Core; + + public class DisposableFieldTests : IDisposable + { + private HttpClient? _httpClient; + + [Before(HookType.Test)] + public void Setup() + { + _httpClient = new HttpClient(); + } + + public void Dispose() + { + _httpClient?.Dispose(); + } + + [Test] + public void Test1() + { + } + } + """ + ); + } + + [Test] + public async Task BeforeTest_No_Issue_When_Disposed_In_DisposeAsync() + { + await Verifier + .VerifyAnalyzerAsync( + """ + using System; + using System.Net.Http; + using System.Threading.Tasks; + using TUnit.Core; + + public class DisposableFieldTests : IAsyncDisposable + { + private HttpClient? _httpClient; + + [Before(HookType.Test)] + public void Setup() + { + _httpClient = new HttpClient(); + } + + public ValueTask DisposeAsync() + { + _httpClient?.Dispose(); + return ValueTask.CompletedTask; + } + + [Test] + public void Test1() + { + } + } + """ + ); + } + + // ======================================== + // IASYNCINITIALIZER ADDITIONAL COMBINATIONS + // ======================================== + + [Test] + public async Task IAsyncInitializer_No_Issue_When_Disposed_In_Dispose() + { + await Verifier + .VerifyAnalyzerAsync( + """ + using System; + using System.Net.Http; + using System.Threading.Tasks; + using TUnit.Core; + using TUnit.Core.Interfaces; + + public class DisposableFieldTests : IAsyncInitializer, IDisposable + { + private HttpClient? _httpClient; + + public Task InitializeAsync() + { + _httpClient = new HttpClient(); + return Task.CompletedTask; + } + + public void Dispose() + { + _httpClient?.Dispose(); + } + + [Test] + public void Test1() + { + } + } + """ + ); + } + + [Test] + public async Task IAsyncInitializer_No_Issue_When_Disposed_In_After() + { + await Verifier + .VerifyAnalyzerAsync( + """ + using System; + using System.Net.Http; + using System.Threading.Tasks; + using TUnit.Core; + using TUnit.Core.Interfaces; + + public class DisposableFieldTests : IAsyncInitializer + { + private HttpClient? _httpClient; + + public Task InitializeAsync() + { + _httpClient = new HttpClient(); + return Task.CompletedTask; + } + + [After(HookType.Test)] + public void Cleanup() + { + _httpClient?.Dispose(); + } + + [Test] + public void Test1() + { + } + } + """ + ); + } + + // ======================================== + // STATIC FIELD WITH BEFORE(ASSEMBLY) TESTS + // ======================================== + + [Test] + public async Task BeforeAssembly_Static_Flags_Issue() + { + await Verifier + .VerifyAnalyzerAsync( + """ + using System.Net.Http; + using TUnit.Core; + + public class DisposableFieldTests + { + private static HttpClient? {|#0:_httpClient|}; + + [Before(HookType.Assembly)] + public static void Setup() + { + _httpClient = new HttpClient(); + } + + [Test] + public void Test1() + { + } + } + """, + + Verifier.Diagnostic(Rules.Dispose_Member_In_Cleanup) + .WithLocation(0) + .WithArguments("_httpClient") + ); + } + + [Test] + public async Task BeforeAssembly_Static_No_Issue_When_Disposed_In_AfterAssembly() + { + await Verifier + .VerifyAnalyzerAsync( + """ + using System.Net.Http; + using TUnit.Core; + + public class DisposableFieldTests + { + private static HttpClient? _httpClient; + + [Before(HookType.Assembly)] + public static void Setup() + { + _httpClient = new HttpClient(); + } + + [After(HookType.Assembly)] + public static void Cleanup() + { + _httpClient?.Dispose(); + } + + [Test] + public void Test1() + { + } + } + """ + ); + } + + // ======================================== + // STATIC FIELD WITH BEFORE(TESTSESSION) TESTS + // ======================================== + + [Test] + public async Task BeforeTestSession_Static_Flags_Issue() + { + await Verifier + .VerifyAnalyzerAsync( + """ + using System.Net.Http; + using TUnit.Core; + + public class DisposableFieldTests + { + private static HttpClient? {|#0:_httpClient|}; + + [Before(HookType.TestSession)] + public static void Setup() + { + _httpClient = new HttpClient(); + } + + [Test] + public void Test1() + { + } + } + """, + + Verifier.Diagnostic(Rules.Dispose_Member_In_Cleanup) + .WithLocation(0) + .WithArguments("_httpClient") + ); + } + + [Test] + public async Task BeforeTestSession_Static_No_Issue_When_Disposed_In_AfterTestSession() + { + await Verifier + .VerifyAnalyzerAsync( + """ + using System.Net.Http; + using TUnit.Core; + + public class DisposableFieldTests + { + private static HttpClient? _httpClient; + + [Before(HookType.TestSession)] + public static void Setup() + { + _httpClient = new HttpClient(); + } + + [After(HookType.TestSession)] + public static void Cleanup() + { + _httpClient?.Dispose(); + } + + [Test] + public void Test1() + { + } + } + """ + ); + } } diff --git a/TUnit.Analyzers/DisposableFieldPropertyAnalyzer.cs b/TUnit.Analyzers/DisposableFieldPropertyAnalyzer.cs index 77900e6c58..151d20de8c 100644 --- a/TUnit.Analyzers/DisposableFieldPropertyAnalyzer.cs +++ b/TUnit.Analyzers/DisposableFieldPropertyAnalyzer.cs @@ -46,6 +46,16 @@ private static void CheckMethods(SyntaxNodeAnalysisContext context, IMethodSymbo var methodSymbols = methods.Where(x => x.IsStatic == isStaticMethod).ToArray(); + // Check field initializers first + if (context.Node is ClassDeclarationSyntax classDeclarationSyntax) + { + var namedTypeSymbol = context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax); + if (namedTypeSymbol != null) + { + CheckFieldInitializers(context, namedTypeSymbol, isStaticMethod, createdObjects); + } + } + foreach (var methodSymbol in methodSymbols) { CheckSetUps(context, methodSymbol, createdObjects); @@ -65,6 +75,87 @@ private static void CheckMethods(SyntaxNodeAnalysisContext context, IMethodSymbo } } + private static void CheckFieldInitializers(SyntaxNodeAnalysisContext context, INamedTypeSymbol namedTypeSymbol, bool isStatic, ConcurrentDictionary createdObjects) + { + // Directly traverse the class syntax to find field declarations + if (context.Node is not ClassDeclarationSyntax classDeclaration) + { + return; + } + + var members = classDeclaration.Members; + + foreach (var member in members) + { + // Handle field declarations: private HttpClient _client = new HttpClient(); + if (member is FieldDeclarationSyntax fieldDeclaration) + { + if (fieldDeclaration.Modifiers.Any(m => m.IsKind(SyntaxKind.StaticKeyword)) != isStatic) + { + continue; + } + + foreach (var variable in fieldDeclaration.Declaration.Variables) + { + if (variable.Initializer == null) + { + continue; + } + + // Check if the initializer contains an object creation expression + var objectCreations = variable.Initializer.Value.DescendantNodesAndSelf() + .OfType(); + + foreach (var objectCreation in objectCreations) + { + var typeInfo = context.SemanticModel.GetTypeInfo(objectCreation); + if (typeInfo.Type?.IsDisposable() is true || typeInfo.Type?.IsAsyncDisposable() is true) + { + var fieldSymbol = context.SemanticModel.GetDeclaredSymbol(variable) as IFieldSymbol; + if (fieldSymbol != null) + { + createdObjects.TryAdd(fieldSymbol, HookLevel.Test); + break; // Only need to add once + } + } + } + } + } + + // Handle property declarations: public HttpClient Client { get; set; } = new HttpClient(); + if (member is PropertyDeclarationSyntax propertyDeclaration) + { + if (propertyDeclaration.Modifiers.Any(m => m.IsKind(SyntaxKind.StaticKeyword)) != isStatic) + { + continue; + } + + if (propertyDeclaration.Initializer == null) + { + continue; + } + + // Check if the initializer contains an object creation expression + var objectCreations = propertyDeclaration.Initializer.Value.DescendantNodesAndSelf() + .OfType(); + + foreach (var objectCreation in objectCreations) + { + var typeInfo = context.SemanticModel.GetTypeInfo(objectCreation); + if (typeInfo.Type?.IsDisposable() is true || typeInfo.Type?.IsAsyncDisposable() is true) + { + var propertySymbol = context.SemanticModel.GetDeclaredSymbol(propertyDeclaration) as IPropertySymbol; + if (propertySymbol != null) + { + createdObjects.TryAdd(propertySymbol, HookLevel.Test); + break; // Only need to add once + } + } + } + } + } + } + private static void CheckSetUps(SyntaxNodeAnalysisContext context, IMethodSymbol methodSymbol, ConcurrentDictionary createdObjects) { var syntaxNodes = methodSymbol.DeclaringSyntaxReferences @@ -72,7 +163,20 @@ private static void CheckSetUps(SyntaxNodeAnalysisContext context, IMethodSymbol var isHookMethod = methodSymbol.IsHookMethod(context.Compilation, out _, out var level, out _); - if (!isHookMethod && methodSymbol.MethodKind != MethodKind.Constructor) + // Check for IAsyncInitializer.InitializeAsync() + var isInitializeAsyncMethod = false; + if (methodSymbol is { Name: "InitializeAsync", Parameters.IsDefaultOrEmpty: true }) + { + var asyncInitializer = context.Compilation.GetTypeByMetadataName("TUnit.Core.Interfaces.IAsyncInitializer"); + if (asyncInitializer != null && methodSymbol.ContainingType.Interfaces.Any(x => + SymbolEqualityComparer.Default.Equals(x, asyncInitializer))) + { + isInitializeAsyncMethod = true; + level = HookLevel.Test; + } + } + + if (!isHookMethod && methodSymbol.MethodKind != MethodKind.Constructor && !isInitializeAsyncMethod) { return; }