From 2a8a58a8bd7fce19e336108f310a288029705166 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 6 Jan 2026 19:45:31 +0000 Subject: [PATCH 1/3] Initial plan From 679de71d89e7b3b06bbb5dc1ae7076e7879634ec Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 6 Jan 2026 19:52:34 +0000 Subject: [PATCH 2/3] Fix TUnit0023 false positive for Func fields Co-authored-by: thomhurst <30480171+thomhurst@users.noreply.github.com> --- .../DisposableFieldPropertyAnalyzerTests.cs | 88 +++++++++++++++++++ .../DisposableFieldPropertyAnalyzer.cs | 8 +- 2 files changed, 94 insertions(+), 2 deletions(-) diff --git a/TUnit.Analyzers.Tests/DisposableFieldPropertyAnalyzerTests.cs b/TUnit.Analyzers.Tests/DisposableFieldPropertyAnalyzerTests.cs index 4b545d6757c..ca52aaa8e16 100644 --- a/TUnit.Analyzers.Tests/DisposableFieldPropertyAnalyzerTests.cs +++ b/TUnit.Analyzers.Tests/DisposableFieldPropertyAnalyzerTests.cs @@ -1197,4 +1197,92 @@ public void Test1() """ ); } + + // ======================================== + // FUNC SHOULD NOT BE FLAGGED + // ======================================== + + [Test] + public async Task Func_Returning_Disposable_No_Issue() + { + await Verifier + .VerifyAnalyzerAsync( + """ + using System; + using System.Net.Http; + using TUnit.Core; + + public interface IMyInterface : IDisposable + { + } + + public class MyClass : IMyInterface + { + public void Dispose() + { + Console.WriteLine("disposed"); + } + } + + public class ExampleTest + { + private readonly Func _factory = () => new MyClass(); + + [Test] + public void Test1() + { + using var t = _factory(); + } + } + """ + ); + } + + [Test] + public async Task Func_Returning_HttpClient_No_Issue() + { + await Verifier + .VerifyAnalyzerAsync( + """ + using System; + using System.Net.Http; + using TUnit.Core; + + public class DisposableFieldTests + { + private readonly Func _clientFactory = () => new HttpClient(); + + [Test] + public void Test1() + { + using var client = _clientFactory(); + } + } + """ + ); + } + + [Test] + public async Task Property_With_Func_Returning_Disposable_No_Issue() + { + await Verifier + .VerifyAnalyzerAsync( + """ + using System; + using System.Net.Http; + using TUnit.Core; + + public class DisposableFieldTests + { + private Func ClientFactory { get; } = () => new HttpClient(); + + [Test] + public void Test1() + { + using var client = ClientFactory(); + } + } + """ + ); + } } diff --git a/TUnit.Analyzers/DisposableFieldPropertyAnalyzer.cs b/TUnit.Analyzers/DisposableFieldPropertyAnalyzer.cs index 151d20de8c9..3dd9f92343d 100644 --- a/TUnit.Analyzers/DisposableFieldPropertyAnalyzer.cs +++ b/TUnit.Analyzers/DisposableFieldPropertyAnalyzer.cs @@ -112,7 +112,9 @@ private static void CheckFieldInitializers(SyntaxNodeAnalysisContext context, IN if (typeInfo.Type?.IsDisposable() is true || typeInfo.Type?.IsAsyncDisposable() is true) { var fieldSymbol = context.SemanticModel.GetDeclaredSymbol(variable) as IFieldSymbol; - if (fieldSymbol != null) + // Only flag if the field type itself is disposable (not e.g. Func) + if (fieldSymbol != null && + (fieldSymbol.Type.IsDisposable() || fieldSymbol.Type.IsAsyncDisposable())) { createdObjects.TryAdd(fieldSymbol, HookLevel.Test); break; // Only need to add once @@ -145,7 +147,9 @@ private static void CheckFieldInitializers(SyntaxNodeAnalysisContext context, IN if (typeInfo.Type?.IsDisposable() is true || typeInfo.Type?.IsAsyncDisposable() is true) { var propertySymbol = context.SemanticModel.GetDeclaredSymbol(propertyDeclaration) as IPropertySymbol; - if (propertySymbol != null) + // Only flag if the property type itself is disposable (not e.g. Func) + if (propertySymbol != null && + (propertySymbol.Type.IsDisposable() || propertySymbol.Type.IsAsyncDisposable())) { createdObjects.TryAdd(propertySymbol, HookLevel.Test); break; // Only need to add once From 6233a5bdca046545df12e16883a076a82c69851e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 6 Jan 2026 19:58:22 +0000 Subject: [PATCH 3/3] Fix null-conditional operator consistency in analyzer Co-authored-by: thomhurst <30480171+thomhurst@users.noreply.github.com> --- TUnit.Analyzers/DisposableFieldPropertyAnalyzer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TUnit.Analyzers/DisposableFieldPropertyAnalyzer.cs b/TUnit.Analyzers/DisposableFieldPropertyAnalyzer.cs index 3dd9f92343d..85d9e921b25 100644 --- a/TUnit.Analyzers/DisposableFieldPropertyAnalyzer.cs +++ b/TUnit.Analyzers/DisposableFieldPropertyAnalyzer.cs @@ -114,7 +114,7 @@ private static void CheckFieldInitializers(SyntaxNodeAnalysisContext context, IN var fieldSymbol = context.SemanticModel.GetDeclaredSymbol(variable) as IFieldSymbol; // Only flag if the field type itself is disposable (not e.g. Func) if (fieldSymbol != null && - (fieldSymbol.Type.IsDisposable() || fieldSymbol.Type.IsAsyncDisposable())) + (fieldSymbol.Type?.IsDisposable() is true || fieldSymbol.Type?.IsAsyncDisposable() is true)) { createdObjects.TryAdd(fieldSymbol, HookLevel.Test); break; // Only need to add once @@ -149,7 +149,7 @@ private static void CheckFieldInitializers(SyntaxNodeAnalysisContext context, IN var propertySymbol = context.SemanticModel.GetDeclaredSymbol(propertyDeclaration) as IPropertySymbol; // Only flag if the property type itself is disposable (not e.g. Func) if (propertySymbol != null && - (propertySymbol.Type.IsDisposable() || propertySymbol.Type.IsAsyncDisposable())) + (propertySymbol.Type?.IsDisposable() is true || propertySymbol.Type?.IsAsyncDisposable() is true)) { createdObjects.TryAdd(propertySymbol, HookLevel.Test); break; // Only need to add once