From e2639a5ef62a7c8a2c5500017156eae3cfa7dfca Mon Sep 17 00:00:00 2001 From: konard Date: Sun, 14 Sep 2025 15:05:57 +0300 Subject: [PATCH 1/4] Initial commit with task details for issue #2 Adding CLAUDE.md with task information for AI processing. This file will be removed when the task is complete. Issue: https://github.com/linksplatform/Disposables/issues/2 --- CLAUDE.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..a310547 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,5 @@ +Issue to solve: https://github.com/linksplatform/Disposables/issues/2 +Your prepared branch: issue-2-3a6319fd +Your prepared working directory: /tmp/gh-issue-solver-1757851553347 + +Proceed. \ No newline at end of file From 2c78c2fb43da103b0cd0d865c973b978439665bf Mon Sep 17 00:00:00 2001 From: konard Date: Sun, 14 Sep 2025 15:20:35 +0300 Subject: [PATCH 2/4] Implement automatic dispose code generation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add AutoDispose attribute for marking classes that need automatic disposal - Create AutoDisposeSourceGenerator that generates disposal code at compile-time - Automatically dispose all IDisposable and Platform.Disposables.IDisposable fields - Handle readonly fields correctly by skipping null assignment - Add comprehensive tests demonstrating functionality - Update project version to 0.5.0 - Fix existing test compatibility with net8 Resolves #2 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../AutoDisposeSourceGenerator.cs | 173 ++++++++++++++++++ ...latform.Disposables.SourceGenerator.csproj | 18 ++ .../AutoDisposeTests.cs | 72 ++++++++ ...m.Disposables.Tests.SourceGenerator.csproj | 24 +++ .../DisposableTests.cs | 2 +- .../AutoDisposeAttribute.cs | 13 ++ .../Platform.Disposables.csproj | 8 +- 7 files changed, 307 insertions(+), 3 deletions(-) create mode 100644 csharp/Platform.Disposables.SourceGenerator/AutoDisposeSourceGenerator.cs create mode 100644 csharp/Platform.Disposables.SourceGenerator/Platform.Disposables.SourceGenerator.csproj create mode 100644 csharp/Platform.Disposables.Tests.SourceGenerator/AutoDisposeTests.cs create mode 100644 csharp/Platform.Disposables.Tests.SourceGenerator/Platform.Disposables.Tests.SourceGenerator.csproj create mode 100644 csharp/Platform.Disposables/AutoDisposeAttribute.cs diff --git a/csharp/Platform.Disposables.SourceGenerator/AutoDisposeSourceGenerator.cs b/csharp/Platform.Disposables.SourceGenerator/AutoDisposeSourceGenerator.cs new file mode 100644 index 0000000..9619f9d --- /dev/null +++ b/csharp/Platform.Disposables.SourceGenerator/AutoDisposeSourceGenerator.cs @@ -0,0 +1,173 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Platform.Disposables.SourceGenerator +{ + [Generator] + public class AutoDisposeSourceGenerator : ISourceGenerator + { + public void Initialize(GeneratorInitializationContext context) + { + context.RegisterForSyntaxNotifications(() => new ClassSyntaxReceiver()); + } + + public void Execute(GeneratorExecutionContext context) + { + if (!(context.SyntaxReceiver is ClassSyntaxReceiver receiver)) + return; + + foreach (var classDeclaration in receiver.CandidateClasses) + { + var semanticModel = context.Compilation.GetSemanticModel(classDeclaration.SyntaxTree); + var classSymbol = semanticModel.GetDeclaredSymbol(classDeclaration); + + if (classSymbol == null) + continue; + + if (!InheritsFromDisposableBase(classSymbol)) + continue; + + if (HasAutoDisposeAttribute(classSymbol)) + { + var sourceCode = GenerateDisposeMethod(classSymbol, classDeclaration); + if (!string.IsNullOrEmpty(sourceCode)) + { + context.AddSource($"{classSymbol.Name}.AutoDispose.g.cs", SourceText.From(sourceCode, Encoding.UTF8)); + } + } + } + } + + private bool InheritsFromDisposableBase(INamedTypeSymbol classSymbol) + { + var baseType = classSymbol.BaseType; + while (baseType != null) + { + if (baseType.Name == "DisposableBase" && + baseType.ContainingNamespace?.ToDisplayString() == "Platform.Disposables") + { + return true; + } + baseType = baseType.BaseType; + } + return false; + } + + private bool HasAutoDisposeAttribute(INamedTypeSymbol classSymbol) + { + return classSymbol.GetAttributes().Any(a => + a.AttributeClass?.Name == "AutoDisposeAttribute" || + a.AttributeClass?.Name == "AutoDispose"); + } + + private string GenerateDisposeMethod(INamedTypeSymbol classSymbol, ClassDeclarationSyntax classDeclaration) + { + var disposableFields = GetDisposableFields(classSymbol); + if (!disposableFields.Any()) + return string.Empty; + + var namespaceName = classSymbol.ContainingNamespace.ToDisplayString(); + var className = classSymbol.Name; + + var disposeStatements = GenerateDisposeStatements(disposableFields); + + var sourceCode = $@"// +#nullable enable +using System; + +namespace {namespaceName} +{{ + partial class {className} + {{ + protected override void Dispose(bool manual, bool wasDisposed) + {{ + if (!wasDisposed) + {{ +{disposeStatements} + }} + }} + }} +}}"; + + return sourceCode; + } + + private List GetDisposableFields(INamedTypeSymbol classSymbol) + { + var disposableFields = new List(); + + foreach (var member in classSymbol.GetMembers()) + { + if (member is IFieldSymbol field) + { + if (IsDisposableType(field.Type)) + { + disposableFields.Add(field); + } + } + } + + return disposableFields; + } + + private bool IsDisposableType(ITypeSymbol typeSymbol) + { + // Check for System.IDisposable + if (ImplementsInterface(typeSymbol, "System", "IDisposable")) + return true; + + // Check for Platform.Disposables.IDisposable + if (ImplementsInterface(typeSymbol, "Platform.Disposables", "IDisposable")) + return true; + + return false; + } + + private bool ImplementsInterface(ITypeSymbol typeSymbol, string namespaceName, string interfaceName) + { + return typeSymbol.AllInterfaces.Any(i => + i.Name == interfaceName && + i.ContainingNamespace?.ToDisplayString() == namespaceName); + } + + private string GenerateDisposeStatements(List disposableFields) + { + var statements = new StringBuilder(); + + foreach (var field in disposableFields) + { + statements.AppendLine($" {field.Name}?.Dispose();"); + // Only set to null if field is not readonly + if (field.Type.CanBeReferencedByName && !field.IsReadOnly) + { + statements.AppendLine($" {field.Name} = null;"); + } + } + + return statements.ToString().TrimEnd(); + } + } + + internal class ClassSyntaxReceiver : ISyntaxReceiver + { + public List CandidateClasses { get; } = new(); + + public void OnVisitSyntaxNode(SyntaxNode syntaxNode) + { + if (syntaxNode is ClassDeclarationSyntax classDeclaration) + { + // Look for classes that might need auto-dispose generation + if (classDeclaration.AttributeLists.Any() || + classDeclaration.BaseList?.Types.Any() == true) + { + CandidateClasses.Add(classDeclaration); + } + } + } + } +} \ No newline at end of file diff --git a/csharp/Platform.Disposables.SourceGenerator/Platform.Disposables.SourceGenerator.csproj b/csharp/Platform.Disposables.SourceGenerator/Platform.Disposables.SourceGenerator.csproj new file mode 100644 index 0000000..52d15ea --- /dev/null +++ b/csharp/Platform.Disposables.SourceGenerator/Platform.Disposables.SourceGenerator.csproj @@ -0,0 +1,18 @@ + + + + netstandard2.0 + latest + enable + false + false + true + true + + + + + + + + \ No newline at end of file diff --git a/csharp/Platform.Disposables.Tests.SourceGenerator/AutoDisposeTests.cs b/csharp/Platform.Disposables.Tests.SourceGenerator/AutoDisposeTests.cs new file mode 100644 index 0000000..e962cf2 --- /dev/null +++ b/csharp/Platform.Disposables.Tests.SourceGenerator/AutoDisposeTests.cs @@ -0,0 +1,72 @@ +using System; +using System.IO; +using Xunit; + +namespace Platform.Disposables.Tests.SourceGenerator +{ + public class AutoDisposeTests + { + [Fact] + public void AutoDispose_DisposesAllFields_WhenObjectIsDisposed() + { + var testDisposable = new TestAutoDispose(); + + Assert.False(testDisposable.IsDisposed); + Assert.False(testDisposable.DisposableField1.IsDisposed); + Assert.False(testDisposable.DisposableField2.IsDisposed); + Assert.False(testDisposable.SystemDisposableField.WasDisposed); + + testDisposable.Dispose(); + + Assert.True(testDisposable.IsDisposed); + Assert.True(testDisposable.DisposableField1.IsDisposed); + Assert.True(testDisposable.DisposableField2.IsDisposed); + Assert.True(testDisposable.SystemDisposableField.WasDisposed); + } + + [Fact] + public void AutoDispose_HandlesNullFields_Gracefully() + { + var testDisposable = new TestAutoDisposeWithNulls(); + + Assert.False(testDisposable.IsDisposed); + + // Should not throw when disposing null fields + testDisposable.Dispose(); + + Assert.True(testDisposable.IsDisposed); + } + } + + [AutoDispose] + public partial class TestAutoDispose : DisposableBase + { + public readonly Disposable DisposableField1; + public readonly Disposable DisposableField2; + public readonly TestSystemDisposable SystemDisposableField; + + public TestAutoDispose() + { + DisposableField1 = new Disposable(); + DisposableField2 = new Disposable(); + SystemDisposableField = new TestSystemDisposable(); + } + } + + [AutoDispose] + public partial class TestAutoDisposeWithNulls : DisposableBase + { + public readonly Disposable? DisposableField1 = null; + public readonly TestSystemDisposable? SystemDisposableField = null; + } + + public class TestSystemDisposable : System.IDisposable + { + public bool WasDisposed { get; private set; } + + public void Dispose() + { + WasDisposed = true; + } + } +} \ No newline at end of file diff --git a/csharp/Platform.Disposables.Tests.SourceGenerator/Platform.Disposables.Tests.SourceGenerator.csproj b/csharp/Platform.Disposables.Tests.SourceGenerator/Platform.Disposables.Tests.SourceGenerator.csproj new file mode 100644 index 0000000..601d2e3 --- /dev/null +++ b/csharp/Platform.Disposables.Tests.SourceGenerator/Platform.Disposables.Tests.SourceGenerator.csproj @@ -0,0 +1,24 @@ + + + + net8 + latest + enable + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + \ No newline at end of file diff --git a/csharp/Platform.Disposables.Tests/DisposableTests.cs b/csharp/Platform.Disposables.Tests/DisposableTests.cs index caa19fb..51867ea 100644 --- a/csharp/Platform.Disposables.Tests/DisposableTests.cs +++ b/csharp/Platform.Disposables.Tests/DisposableTests.cs @@ -42,7 +42,7 @@ private static ProcessStartInfo CreateProcessStartInfo(string logPath, bool wait return new ProcessStartInfo { FileName = "dotnet", - Arguments = $"run -p \"{projectPath}\" -f net7 \"{logPath}\" {waitForCancellation.ToString()}", + Arguments = $"run -p \"{projectPath}\" -f net8 \"{logPath}\" {waitForCancellation.ToString()}", UseShellExecute = false, CreateNoWindow = true }; diff --git a/csharp/Platform.Disposables/AutoDisposeAttribute.cs b/csharp/Platform.Disposables/AutoDisposeAttribute.cs new file mode 100644 index 0000000..beae4f5 --- /dev/null +++ b/csharp/Platform.Disposables/AutoDisposeAttribute.cs @@ -0,0 +1,13 @@ +using System; + +namespace Platform.Disposables +{ + /// <summary> + /// <para>Indicates that the class should have automatic dispose code generation for all disposable fields.</para> + /// <para>Указывает, что для класса должен быть автоматически сгенерирован код для высвобождения всех высвобождаемых полей.</para> + /// </summary> + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] + public sealed class AutoDisposeAttribute : Attribute + { + } +} \ No newline at end of file diff --git a/csharp/Platform.Disposables/Platform.Disposables.csproj b/csharp/Platform.Disposables/Platform.Disposables.csproj index 957fb95..46be3f0 100644 --- a/csharp/Platform.Disposables/Platform.Disposables.csproj +++ b/csharp/Platform.Disposables/Platform.Disposables.csproj @@ -4,7 +4,7 @@ LinksPlatform's Platform.Disposables Class Library Konstantin Diachenko Platform.Disposables - 0.4.0 + 0.5.0 Konstantin Diachenko net8 Platform.Disposables @@ -23,7 +23,7 @@ true snupkg latest - Update target framework from net7 to net8. + Add AutoDispose source generator for automatic dispose code generation. enable @@ -39,6 +39,10 @@ + + + +