Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions AUTODISPOSE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# AutoDispose Source Generator

This feature implements automatic dispose code generation for classes inheriting from `DisposableBase`.

## Usage

1. Apply the `[AutoDispose]` attribute to your class
2. Make the class `partial`
3. The source generator will automatically create a `Dispose(bool manual, bool wasDisposed)` method that:
- Finds all fields implementing `IDisposable` or `Platform.Disposables.IDisposable`
- Calls `Dispose()` on each field using null-conditional operator (`?.`)
- Sets non-readonly fields to `null` after disposal

## Example

```csharp
[AutoDispose]
public partial class MyClass : DisposableBase
{
private readonly FileStream _readOnlyField;
private MemoryStream _writableField;

// Generated Dispose method will be:
// protected override void Dispose(bool manual, bool wasDisposed)
// {
// if (!wasDisposed)
// {
// _readOnlyField?.Dispose();
// _writableField?.Dispose();
// _writableField = null;
// }
// }
}
```

## Benefits

- Reduces boilerplate code
- Prevents memory leaks from forgotten disposals
- Automatically handles null checks
- Respects readonly field constraints
- Compile-time code generation for zero runtime overhead
Original file line number Diff line number Diff line change
@@ -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 = $@"// <auto-generated />
#nullable enable
using System;

namespace {namespaceName}
{{
partial class {className}
{{
protected override void Dispose(bool manual, bool wasDisposed)
{{
if (!wasDisposed)
{{
{disposeStatements}
}}
}}
}}
}}";

return sourceCode;
}

private List<IFieldSymbol> GetDisposableFields(INamedTypeSymbol classSymbol)
{
var disposableFields = new List<IFieldSymbol>();

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<IFieldSymbol> 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<ClassDeclarationSyntax> 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);
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<IncludeBuildOutput>false</IncludeBuildOutput>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
<IsRoslynComponent>true</IsRoslynComponent>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="../Platform.Disposables/Platform.Disposables.csproj" />
<Analyzer Include="../Platform.Disposables.SourceGenerator/bin/$(Configuration)/netstandard2.0/Platform.Disposables.SourceGenerator.dll" />
</ItemGroup>

</Project>
2 changes: 1 addition & 1 deletion csharp/Platform.Disposables.Tests/DisposableTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
Expand Down
13 changes: 13 additions & 0 deletions csharp/Platform.Disposables/AutoDisposeAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System;

namespace Platform.Disposables
{
/// &lt;summary&gt;
/// &lt;para&gt;Indicates that the class should have automatic dispose code generation for all disposable fields.&lt;/para&gt;
/// &lt;para&gt;Указывает, что для класса должен быть автоматически сгенерирован код для высвобождения всех высвобождаемых полей.&lt;/para&gt;
/// &lt;/summary&gt;
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public sealed class AutoDisposeAttribute : Attribute
{
}
}
8 changes: 6 additions & 2 deletions csharp/Platform.Disposables/Platform.Disposables.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<Description>LinksPlatform's Platform.Disposables Class Library</Description>
<Copyright>Konstantin Diachenko</Copyright>
<AssemblyTitle>Platform.Disposables</AssemblyTitle>
<VersionPrefix>0.4.0</VersionPrefix>
<VersionPrefix>0.5.0</VersionPrefix>
<Authors>Konstantin Diachenko</Authors>
<TargetFramework>net8</TargetFramework>
<AssemblyName>Platform.Disposables</AssemblyName>
Expand All @@ -23,7 +23,7 @@
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<LangVersion>latest</LangVersion>
<PackageReleaseNotes>Update target framework from net7 to net8.</PackageReleaseNotes>
<PackageReleaseNotes>Add AutoDispose source generator for automatic dispose code generation.</PackageReleaseNotes>
<Nullable>enable</Nullable>
</PropertyGroup>

Expand All @@ -39,6 +39,10 @@
<PackageReference Include="Platform.Exceptions" Version="0.5.0" />
</ItemGroup>

<ItemGroup>
<Analyzer Include="../Platform.Disposables.SourceGenerator/bin/$(Configuration)/netstandard2.0/Platform.Disposables.SourceGenerator.dll" />
</ItemGroup>

<!--


Expand Down
Loading
Loading