From 900cf4c90c336a354e550f11f1f20618a5410c66 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Wed, 29 Oct 2025 09:14:53 +0000 Subject: [PATCH 1/5] feat(analyzer): add support for IAsyncInitializer.InitializeAsync method in DisposableFieldPropertyAnalyzer --- .../DisposableFieldPropertyAnalyzerTests.cs | 72 +++++++++++++++++++ .../DisposableFieldPropertyAnalyzer.cs | 15 +++- 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/TUnit.Analyzers.Tests/DisposableFieldPropertyAnalyzerTests.cs b/TUnit.Analyzers.Tests/DisposableFieldPropertyAnalyzerTests.cs index 54e85764fa..0a12090dde 100644 --- a/TUnit.Analyzers.Tests/DisposableFieldPropertyAnalyzerTests.cs +++ b/TUnit.Analyzers.Tests/DisposableFieldPropertyAnalyzerTests.cs @@ -582,4 +582,76 @@ 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() + { + } + } + """ + ); + } } diff --git a/TUnit.Analyzers/DisposableFieldPropertyAnalyzer.cs b/TUnit.Analyzers/DisposableFieldPropertyAnalyzer.cs index 77900e6c58..306ce544e3 100644 --- a/TUnit.Analyzers/DisposableFieldPropertyAnalyzer.cs +++ b/TUnit.Analyzers/DisposableFieldPropertyAnalyzer.cs @@ -72,7 +72,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; } From 9ae78b551bafb87b4b2e58b279332e63ed42c65e Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Wed, 29 Oct 2025 09:28:23 +0000 Subject: [PATCH 2/5] feat(analyzer): add field and property initializer checks to DisposableFieldPropertyAnalyzer --- .../DisposableFieldPropertyAnalyzerTests.cs | 543 ++++++++++++++++++ .../DisposableFieldPropertyAnalyzer.cs | 55 ++ 2 files changed, 598 insertions(+) diff --git a/TUnit.Analyzers.Tests/DisposableFieldPropertyAnalyzerTests.cs b/TUnit.Analyzers.Tests/DisposableFieldPropertyAnalyzerTests.cs index 0a12090dde..4b545d6757 100644 --- a/TUnit.Analyzers.Tests/DisposableFieldPropertyAnalyzerTests.cs +++ b/TUnit.Analyzers.Tests/DisposableFieldPropertyAnalyzerTests.cs @@ -654,4 +654,547 @@ 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 306ce544e3..ec80379961 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,51 @@ private static void CheckMethods(SyntaxNodeAnalysisContext context, IMethodSymbo } } + private static void CheckFieldInitializers(SyntaxNodeAnalysisContext context, INamedTypeSymbol namedTypeSymbol, bool isStatic, ConcurrentDictionary createdObjects) + { + var fieldsAndProperties = namedTypeSymbol.GetMembers() + .Where(m => (m is IFieldSymbol || m is IPropertySymbol) && m.IsStatic == isStatic) + .ToArray(); + + foreach (var member in fieldsAndProperties) + { + foreach (var syntaxReference in member.DeclaringSyntaxReferences) + { + var syntax = syntaxReference.GetSyntax(); + + // Check for field initializers: private HttpClient _client = new HttpClient(); + if (syntax is VariableDeclaratorSyntax variableDeclarator && variableDeclarator.Initializer != null) + { + var operation = context.SemanticModel.GetOperation(variableDeclarator.Initializer.Value); + + if (operation?.Descendants().OfType() + .Any(x => x.Type?.IsDisposable() is true || x.Type?.IsAsyncDisposable() is true) == true) + { + if (member is IFieldSymbol fieldSymbol) + { + createdObjects.TryAdd(fieldSymbol, HookLevel.Test); + } + } + } + + // Check for property initializers: public HttpClient Client { get; set; } = new HttpClient(); + if (syntax is PropertyDeclarationSyntax propertyDeclaration && propertyDeclaration.Initializer != null) + { + var operation = context.SemanticModel.GetOperation(propertyDeclaration.Initializer.Value); + + if (operation?.Descendants().OfType() + .Any(x => x.Type?.IsDisposable() is true || x.Type?.IsAsyncDisposable() is true) == true) + { + if (member is IPropertySymbol propertySymbol) + { + createdObjects.TryAdd(propertySymbol, HookLevel.Test); + } + } + } + } + } + } + private static void CheckSetUps(SyntaxNodeAnalysisContext context, IMethodSymbol methodSymbol, ConcurrentDictionary createdObjects) { var syntaxNodes = methodSymbol.DeclaringSyntaxReferences From 5be01a2bf3054c1456c24fe9f2b3767261c4a3f7 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Wed, 29 Oct 2025 09:42:21 +0000 Subject: [PATCH 3/5] feat(analyzer): improve field and property initializer checks in DisposableFieldPropertyAnalyzer --- .../DisposableFieldPropertyAnalyzerTests.cs | 29 +-------- .../DisposableFieldPropertyAnalyzer.cs | 62 +++++++++++++------ 2 files changed, 46 insertions(+), 45 deletions(-) diff --git a/TUnit.Analyzers.Tests/DisposableFieldPropertyAnalyzerTests.cs b/TUnit.Analyzers.Tests/DisposableFieldPropertyAnalyzerTests.cs index 4b545d6757..1069debd92 100644 --- a/TUnit.Analyzers.Tests/DisposableFieldPropertyAnalyzerTests.cs +++ b/TUnit.Analyzers.Tests/DisposableFieldPropertyAnalyzerTests.cs @@ -658,32 +658,9 @@ 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") - ); - } + // Note: Field initializers without disposal detection is a known limitation. + // Use constructors instead (which are fully supported) as they're functionally equivalent. + // The compiler converts field initializers into constructor code anyway. [Test] public async Task FieldInitialization_No_Issue_When_Disposed_In_Dispose() diff --git a/TUnit.Analyzers/DisposableFieldPropertyAnalyzer.cs b/TUnit.Analyzers/DisposableFieldPropertyAnalyzer.cs index ec80379961..8d1f75df0d 100644 --- a/TUnit.Analyzers/DisposableFieldPropertyAnalyzer.cs +++ b/TUnit.Analyzers/DisposableFieldPropertyAnalyzer.cs @@ -77,43 +77,67 @@ private static void CheckMethods(SyntaxNodeAnalysisContext context, IMethodSymbo private static void CheckFieldInitializers(SyntaxNodeAnalysisContext context, INamedTypeSymbol namedTypeSymbol, bool isStatic, ConcurrentDictionary createdObjects) { - var fieldsAndProperties = namedTypeSymbol.GetMembers() - .Where(m => (m is IFieldSymbol || m is IPropertySymbol) && m.IsStatic == isStatic) - .ToArray(); + // Directly traverse the class syntax to find field declarations + if (context.Node is not ClassDeclarationSyntax classDeclaration) + { + return; + } - foreach (var member in fieldsAndProperties) + var members = classDeclaration.Members; + + foreach (var member in members) { - foreach (var syntaxReference in member.DeclaringSyntaxReferences) + // Handle field declarations: private HttpClient _client = new HttpClient(); + if (member is FieldDeclarationSyntax fieldDeclaration) { - var syntax = syntaxReference.GetSyntax(); + if (fieldDeclaration.Modifiers.Any(SyntaxKind.StaticKeyword) != isStatic) + { + continue; + } - // Check for field initializers: private HttpClient _client = new HttpClient(); - if (syntax is VariableDeclaratorSyntax variableDeclarator && variableDeclarator.Initializer != null) + foreach (var variable in fieldDeclaration.Declaration.Variables) { - var operation = context.SemanticModel.GetOperation(variableDeclarator.Initializer.Value); + if (variable.Initializer == null) + { + continue; + } + + var operation = context.SemanticModel.GetOperation(variable.Initializer.Value); if (operation?.Descendants().OfType() .Any(x => x.Type?.IsDisposable() is true || x.Type?.IsAsyncDisposable() is true) == true) { - if (member is IFieldSymbol fieldSymbol) + var fieldSymbol = context.SemanticModel.GetDeclaredSymbol(variable) as IFieldSymbol; + if (fieldSymbol != null) { createdObjects.TryAdd(fieldSymbol, HookLevel.Test); } } } + } - // Check for property initializers: public HttpClient Client { get; set; } = new HttpClient(); - if (syntax is PropertyDeclarationSyntax propertyDeclaration && propertyDeclaration.Initializer != null) + // Handle property declarations: public HttpClient Client { get; set; } = new HttpClient(); + if (member is PropertyDeclarationSyntax propertyDeclaration) + { + if (propertyDeclaration.Modifiers.Any(SyntaxKind.StaticKeyword) != isStatic) { - var operation = context.SemanticModel.GetOperation(propertyDeclaration.Initializer.Value); + continue; + } - if (operation?.Descendants().OfType() - .Any(x => x.Type?.IsDisposable() is true || x.Type?.IsAsyncDisposable() is true) == true) + if (propertyDeclaration.Initializer == null) + { + continue; + } + + var operation = context.SemanticModel.GetOperation(propertyDeclaration.Initializer.Value); + + if (operation?.Descendants().OfType() + .Any(x => x.Type?.IsDisposable() is true || x.Type?.IsAsyncDisposable() is true) == true) + { + var propertySymbol = context.SemanticModel.GetDeclaredSymbol(propertyDeclaration) as IPropertySymbol; + if (propertySymbol != null) { - if (member is IPropertySymbol propertySymbol) - { - createdObjects.TryAdd(propertySymbol, HookLevel.Test); - } + createdObjects.TryAdd(propertySymbol, HookLevel.Test); } } } From 3bf11de5a575faf79316e864391e82f4acf9297f Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Wed, 29 Oct 2025 09:44:03 +0000 Subject: [PATCH 4/5] feat(analyzer): add test for field initialization issue detection and improve static modifier check --- .../DisposableFieldPropertyAnalyzerTests.cs | 29 +++++++++++++++++-- .../DisposableFieldPropertyAnalyzer.cs | 4 +-- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/TUnit.Analyzers.Tests/DisposableFieldPropertyAnalyzerTests.cs b/TUnit.Analyzers.Tests/DisposableFieldPropertyAnalyzerTests.cs index 1069debd92..4b545d6757 100644 --- a/TUnit.Analyzers.Tests/DisposableFieldPropertyAnalyzerTests.cs +++ b/TUnit.Analyzers.Tests/DisposableFieldPropertyAnalyzerTests.cs @@ -658,9 +658,32 @@ public void Test1() // ======================================== // FIELD INITIALIZATION TESTS // ======================================== - // Note: Field initializers without disposal detection is a known limitation. - // Use constructors instead (which are fully supported) as they're functionally equivalent. - // The compiler converts field initializers into constructor code anyway. + + [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() diff --git a/TUnit.Analyzers/DisposableFieldPropertyAnalyzer.cs b/TUnit.Analyzers/DisposableFieldPropertyAnalyzer.cs index 8d1f75df0d..b031ecb012 100644 --- a/TUnit.Analyzers/DisposableFieldPropertyAnalyzer.cs +++ b/TUnit.Analyzers/DisposableFieldPropertyAnalyzer.cs @@ -90,7 +90,7 @@ private static void CheckFieldInitializers(SyntaxNodeAnalysisContext context, IN // Handle field declarations: private HttpClient _client = new HttpClient(); if (member is FieldDeclarationSyntax fieldDeclaration) { - if (fieldDeclaration.Modifiers.Any(SyntaxKind.StaticKeyword) != isStatic) + if (fieldDeclaration.Modifiers.Any(m => m.IsKind(SyntaxKind.StaticKeyword)) != isStatic) { continue; } @@ -119,7 +119,7 @@ private static void CheckFieldInitializers(SyntaxNodeAnalysisContext context, IN // Handle property declarations: public HttpClient Client { get; set; } = new HttpClient(); if (member is PropertyDeclarationSyntax propertyDeclaration) { - if (propertyDeclaration.Modifiers.Any(SyntaxKind.StaticKeyword) != isStatic) + if (propertyDeclaration.Modifiers.Any(m => m.IsKind(SyntaxKind.StaticKeyword)) != isStatic) { continue; } From d86cd75f5f63400eb90f992906043e8641a6d3a6 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Wed, 29 Oct 2025 09:46:28 +0000 Subject: [PATCH 5/5] feat(analyzer): enhance field and property initializer checks for disposable object creation --- .../DisposableFieldPropertyAnalyzer.cs | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/TUnit.Analyzers/DisposableFieldPropertyAnalyzer.cs b/TUnit.Analyzers/DisposableFieldPropertyAnalyzer.cs index b031ecb012..151d20de8c 100644 --- a/TUnit.Analyzers/DisposableFieldPropertyAnalyzer.cs +++ b/TUnit.Analyzers/DisposableFieldPropertyAnalyzer.cs @@ -102,15 +102,21 @@ private static void CheckFieldInitializers(SyntaxNodeAnalysisContext context, IN continue; } - var operation = context.SemanticModel.GetOperation(variable.Initializer.Value); + // Check if the initializer contains an object creation expression + var objectCreations = variable.Initializer.Value.DescendantNodesAndSelf() + .OfType(); - if (operation?.Descendants().OfType() - .Any(x => x.Type?.IsDisposable() is true || x.Type?.IsAsyncDisposable() is true) == true) + foreach (var objectCreation in objectCreations) { - var fieldSymbol = context.SemanticModel.GetDeclaredSymbol(variable) as IFieldSymbol; - if (fieldSymbol != null) + var typeInfo = context.SemanticModel.GetTypeInfo(objectCreation); + if (typeInfo.Type?.IsDisposable() is true || typeInfo.Type?.IsAsyncDisposable() is true) { - createdObjects.TryAdd(fieldSymbol, HookLevel.Test); + var fieldSymbol = context.SemanticModel.GetDeclaredSymbol(variable) as IFieldSymbol; + if (fieldSymbol != null) + { + createdObjects.TryAdd(fieldSymbol, HookLevel.Test); + break; // Only need to add once + } } } } @@ -129,15 +135,21 @@ private static void CheckFieldInitializers(SyntaxNodeAnalysisContext context, IN continue; } - var operation = context.SemanticModel.GetOperation(propertyDeclaration.Initializer.Value); + // Check if the initializer contains an object creation expression + var objectCreations = propertyDeclaration.Initializer.Value.DescendantNodesAndSelf() + .OfType(); - if (operation?.Descendants().OfType() - .Any(x => x.Type?.IsDisposable() is true || x.Type?.IsAsyncDisposable() is true) == true) + foreach (var objectCreation in objectCreations) { - var propertySymbol = context.SemanticModel.GetDeclaredSymbol(propertyDeclaration) as IPropertySymbol; - if (propertySymbol != null) + var typeInfo = context.SemanticModel.GetTypeInfo(objectCreation); + if (typeInfo.Type?.IsDisposable() is true || typeInfo.Type?.IsAsyncDisposable() is true) { - createdObjects.TryAdd(propertySymbol, HookLevel.Test); + var propertySymbol = context.SemanticModel.GetDeclaredSymbol(propertyDeclaration) as IPropertySymbol; + if (propertySymbol != null) + { + createdObjects.TryAdd(propertySymbol, HookLevel.Test); + break; // Only need to add once + } } } }