diff --git a/src/IDisposableGenerator/ClassItems.cs b/src/IDisposableGenerator/ClassItems.cs index 4d19549..a21c35f 100644 --- a/src/IDisposableGenerator/ClassItems.cs +++ b/src/IDisposableGenerator/ClassItems.cs @@ -5,6 +5,7 @@ internal class ClassItems public string? Name { get; set; } public Accessibility Accessibility { get; set; } public bool Stream { get; set; } + public bool WithoutThrowIfDisposed { get; set; } public List Owns { get; } = []; public List Fields { get; } = []; public List SetNull { get; } = []; @@ -46,6 +47,7 @@ public override string ToString() _ = result.Append($"Class: Name {this.Name}") .Append($", Accessibility: {this.Accessibility}") .Append($", Stream: {this.Stream}") + .Append($", Without ThrowIfDisposed: {this.WithoutThrowIfDisposed}") .Append($", Owns Count: {this.Owns.Count}") .Append($", Fields Count: {this.Fields.Count}") .Append($", SetNull Count: {this.SetNull.Count}") diff --git a/src/IDisposableGenerator/DisposableCodeWriter.cs b/src/IDisposableGenerator/DisposableCodeWriter.cs index 0f2a723..1615f1c 100644 --- a/src/IDisposableGenerator/DisposableCodeWriter.cs +++ b/src/IDisposableGenerator/DisposableCodeWriter.cs @@ -101,9 +101,28 @@ End If "); } - _ = sourceBuilder.Append(@" End Sub - End Class -"); + _ = sourceBuilder.Append(""" + End Sub + + """); + + if (!classItem.WithoutThrowIfDisposed) + { + _ = sourceBuilder.Append($$""" + + Friend Sub ThrowIfDisposed() + If Me.isDisposed Then + Throw New ObjectDisposedException(NameOf({{classItem.Name}})) + End If + End Sub + + """); + } + + _ = sourceBuilder.Append(""" + End Class + + """); } _ = sourceBuilder.Append(@"End Namespace @@ -209,9 +228,30 @@ namespace {workItem.Namespace}; "); } - _ = sourceBuilder.Append(@" } -} -"); + _ = sourceBuilder.Append(""" + } + + """); + + if (!classItem.WithoutThrowIfDisposed) + { + _ = sourceBuilder.Append($$""" + + internal void ThrowIfDisposed() + { + if (this.isDisposed) + { + throw new ObjectDisposedException(nameof({{classItem.Name}})); + } + } + + """); + } + + _ = sourceBuilder.Append(""" + } + + """); } // inject the created sources into the users compilation. @@ -319,13 +359,36 @@ namespace {workItem.Namespace} "); } - _ = sourceBuilder.Append(@" } - } -"); + _ = sourceBuilder.Append(""" + } + + """); + + if (!classItem.WithoutThrowIfDisposed) + { + _ = sourceBuilder.Append($$""" + + internal void ThrowIfDisposed() + { + if (this.isDisposed) + { + throw new ObjectDisposedException(nameof({{classItem.Name}})); + } + } + + """); + } + + _ = sourceBuilder.Append(""" + } + + """); } - _ = sourceBuilder.Append(@"} -"); + _ = sourceBuilder.Append(""" + } + + """); } // inject the created source into the users compilation. diff --git a/src/IDisposableGenerator/Properties/Resources.resx b/src/IDisposableGenerator/Properties/Resources.resx index ef047f3..7c16a63 100644 --- a/src/IDisposableGenerator/Properties/Resources.resx +++ b/src/IDisposableGenerator/Properties/Resources.resx @@ -1,25 +1,124 @@ - - - - - - - - text/microsoft-resx - - - 1.3 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - // <autogenerated/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + // <autogenerated/> #pragma warning disable SA1636, 8618 namespace IDisposableGenerator { @@ -60,12 +159,18 @@ namespace IDisposableGenerator { } } + + // used only by a source generator to generate Dispose() and Dispose(bool). + [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + internal class WithoutThrowIfDisposedAttribute : Attribute + { + } } #pragma warning restore SA1636, 8618 - - - ' <autogenerated/> + + + ' <autogenerated/> #Disable Warning SA1636 Imports System @@ -101,8 +206,14 @@ Namespace IDisposableGenerator Inherits Attribute End Class + ' used only by a source generator to generate Dispose() and Dispose(bool). + <AttributeUsage(AttributeTargets.Class, Inherited:=False, AllowMultiple:=False)> + Friend Class WithoutThrowIfDisposedAttribute + Inherits Attribute + End Class + End Namespace #Enable Warning SA1636 - + \ No newline at end of file diff --git a/src/IDisposableGenerator/WorkItemCollection.cs b/src/IDisposableGenerator/WorkItemCollection.cs index 9530cb6..bcf0d52 100644 --- a/src/IDisposableGenerator/WorkItemCollection.cs +++ b/src/IDisposableGenerator/WorkItemCollection.cs @@ -22,19 +22,19 @@ public void Process(INamedTypeSymbol testClass, CancellationToken ct) } ct.ThrowIfCancellationRequested(); - var classItemsQuery = - from att in testClass.GetAttributes() - where att.AttributeClass!.Name.Equals( - "GenerateDisposeAttribute", StringComparison.Ordinal) - select GetClassItem(att, testClass); + var classItem = GetClassItem(testClass); + + if (classItem is null) + { + return; + } + + ct.ThrowIfCancellationRequested(); + workItem!.Classes.Add(classItem); + var memberQuery = from member in testClass.GetMembers() select member; - foreach (var classItem in classItemsQuery) - { - ct.ThrowIfCancellationRequested(); - workItem!.Classes.Add(classItem); - } foreach (var member in memberQuery) { @@ -49,16 +49,30 @@ public List GetWorkItems() public int IndexOf(WorkItem item) => this.WorkItems.IndexOf(item); - private static ClassItems GetClassItem(AttributeData attr, INamedTypeSymbol testClass) + private static ClassItems? GetClassItem(INamedTypeSymbol testClass) { - var result = new ClassItems + var result = new ClassItems(); + var hasDisposalGeneration = false; + + foreach (var attr in testClass.GetAttributes()) { - Name = testClass.Name, - Accessibility = testClass.DeclaredAccessibility, - Stream = (bool)attr.ConstructorArguments[0].Value!, - }; +#pragma warning disable IDE0010 // Add missing cases + switch (attr.AttributeClass!.Name) + { + case "GenerateDisposeAttribute": + hasDisposalGeneration = true; + result.Name = testClass.Name; + result.Accessibility = testClass.DeclaredAccessibility; + result.Stream = (bool)attr.ConstructorArguments[0].Value!; + break; + case "WithoutThrowIfDisposedAttribute": + result.WithoutThrowIfDisposed = true; + break; + } +#pragma warning restore IDE0010 // Add missing cases + } - return result; + return hasDisposalGeneration ? result : null; } private static void CheckAttributesOnMember(ISymbol member, diff --git a/tests/IDisposableGeneratorTests.CSharp10.cs b/tests/IDisposableGeneratorTests.CSharp10.cs index e329199..3145737 100644 --- a/tests/IDisposableGeneratorTests.CSharp10.cs +++ b/tests/IDisposableGeneratorTests.CSharp10.cs @@ -26,6 +26,14 @@ private void Dispose(bool disposing) this.isDisposed = true; } } + + internal void ThrowIfDisposed() + { + if (this.isDisposed) + { + throw new ObjectDisposedException(nameof(TestDisposable)); + } + } } ", @"global using System; global using IDisposableGenerator; @@ -67,6 +75,14 @@ private void Dispose(bool disposing) this.isDisposed = true; } } + + internal void ThrowIfDisposed() + { + if (this.isDisposed) + { + throw new ObjectDisposedException(nameof(TestDisposable)); + } + } } ", @"global using System; global using IDisposableGenerator; @@ -113,6 +129,14 @@ private void Dispose(bool disposing) this.isDisposed = true; } } + + internal void ThrowIfDisposed() + { + if (this.isDisposed) + { + throw new ObjectDisposedException(nameof(TestDisposable)); + } + } } ", @"global using System; global using IDisposableGenerator; @@ -153,6 +177,14 @@ protected override void Dispose(bool disposing) // On Streams call base.Dispose(disposing)!!! base.Dispose(disposing); } + + internal void ThrowIfDisposed() + { + if (this.isDisposed) + { + throw new ObjectDisposedException(nameof(TestDisposable)); + } + } } ", @"global using System; global using System.IO; @@ -210,6 +242,14 @@ protected override void Dispose(bool disposing) // On Streams call base.Dispose(disposing)!!! base.Dispose(disposing); } + + internal void ThrowIfDisposed() + { + if (this.isDisposed) + { + throw new ObjectDisposedException(nameof(TestDisposable)); + } + } } ", @"global using System; global using System.IO; @@ -264,6 +304,14 @@ private void Dispose(bool disposing) this.isDisposed = true; } } + + internal void ThrowIfDisposed() + { + if (this.isDisposed) + { + throw new ObjectDisposedException(nameof(TestDisposable)); + } + } } ", @"global using System; global using IDisposableGenerator; @@ -310,6 +358,14 @@ private void Dispose(bool disposing) this.isDisposed = true; } } + + internal void ThrowIfDisposed() + { + if (this.isDisposed) + { + throw new ObjectDisposedException(nameof(TestDisposable)); + } + } } ", @"global using System; global using System.ComponentModel.DataAnnotations; @@ -361,6 +417,14 @@ private void Dispose(bool disposing) this.isDisposed = true; } } + + internal void ThrowIfDisposed() + { + if (this.isDisposed) + { + throw new ObjectDisposedException(nameof(TestDisposable)); + } + } } "}, {"Disposables.1.g.cs", @"// @@ -382,9 +446,18 @@ private void Dispose(bool disposing) this.isDisposed = true; } } + + internal void ThrowIfDisposed() + { + if (this.isDisposed) + { + throw new ObjectDisposedException(nameof(AnotherDisposable)); + } + } } "} }; + await RunTest(@"// namespace MyApp; @@ -405,6 +478,14 @@ private void Dispose(bool disposing) this.isDisposed = true; } } + + internal void ThrowIfDisposed() + { + if (this.isDisposed) + { + throw new ObjectDisposedException(nameof(TestDisposable)); + } + } } ", @"global using System; global using System.ComponentModel.DataAnnotations; @@ -420,4 +501,52 @@ internal partial class TestDisposable } ", LanguageVersion.CSharp10, testSources, generatedSources); } + + [Fact] + public async Task TestWithoutThrowIfDisposedCSharp10() + { + const string generatedSource = """ + // + namespace MyApp; + + internal partial class TestDisposable : IDisposable + { + private bool isDisposed; + + /// + /// Cleans up the resources used by . + /// + public void Dispose() => this.Dispose(true); + + private void Dispose(bool disposing) + { + if (!this.isDisposed && disposing) + { + this.test = null; + this.isDisposed = true; + } + } + } + + """; + + const string testSource = """ + global using System; + global using System.ComponentModel.DataAnnotations; + global using IDisposableGenerator; + + namespace MyApp; + + [GenerateDispose(false)] + [WithoutThrowIfDisposed] + internal partial class TestDisposable + { + [NullOnDispose] + public string? test { get; set; } = "stuff here."; + } + + """; + + await RunTest(generatedSource, testSource, LanguageVersion.CSharp10); + } } diff --git a/tests/IDisposableGeneratorTests.CSharp9.cs b/tests/IDisposableGeneratorTests.CSharp9.cs index 8b8e59b..5633342 100644 --- a/tests/IDisposableGeneratorTests.CSharp9.cs +++ b/tests/IDisposableGeneratorTests.CSharp9.cs @@ -28,6 +28,14 @@ private void Dispose(bool disposing) this.isDisposed = true; } } + + internal void ThrowIfDisposed() + { + if (this.isDisposed) + { + throw new ObjectDisposedException(nameof(TestDisposable)); + } + } } } ", @"namespace MyApp @@ -73,6 +81,14 @@ private void Dispose(bool disposing) this.isDisposed = true; } } + + internal void ThrowIfDisposed() + { + if (this.isDisposed) + { + throw new ObjectDisposedException(nameof(TestDisposable)); + } + } } } ", @"namespace MyApp @@ -123,6 +139,14 @@ private void Dispose(bool disposing) this.isDisposed = true; } } + + internal void ThrowIfDisposed() + { + if (this.isDisposed) + { + throw new ObjectDisposedException(nameof(TestDisposable)); + } + } } } ", @"namespace MyApp @@ -167,6 +191,14 @@ protected override void Dispose(bool disposing) // On Streams call base.Dispose(disposing)!!! base.Dispose(disposing); } + + internal void ThrowIfDisposed() + { + if (this.isDisposed) + { + throw new ObjectDisposedException(nameof(TestDisposable)); + } + } } } ", @"namespace MyApp @@ -228,6 +260,14 @@ protected override void Dispose(bool disposing) // On Streams call base.Dispose(disposing)!!! base.Dispose(disposing); } + + internal void ThrowIfDisposed() + { + if (this.isDisposed) + { + throw new ObjectDisposedException(nameof(TestDisposable)); + } + } } } ", @"namespace MyApp @@ -286,6 +326,14 @@ private void Dispose(bool disposing) this.isDisposed = true; } } + + internal void ThrowIfDisposed() + { + if (this.isDisposed) + { + throw new ObjectDisposedException(nameof(TestDisposable)); + } + } } } ", @"namespace MyApp @@ -336,6 +384,14 @@ private void Dispose(bool disposing) this.isDisposed = true; } } + + internal void ThrowIfDisposed() + { + if (this.isDisposed) + { + throw new ObjectDisposedException(nameof(TestDisposable)); + } + } } } ", @"namespace MyApp @@ -353,4 +409,57 @@ internal partial class TestDisposable } } "); + + [Fact] + public async Task TestWithoutThrowIfDisposedCSharp9() + { + const string generatedSource = """ + // + namespace MyApp + { + using global::System; + + internal partial class TestDisposable : IDisposable + { + private bool isDisposed; + + /// + /// Cleans up the resources used by . + /// + public void Dispose() => this.Dispose(true); + + private void Dispose(bool disposing) + { + if (!this.isDisposed && disposing) + { + this.test = null; + this.isDisposed = true; + } + } + } + } + + """; + + const string testSource = """ + namespace MyApp + { + using System; + using System.ComponentModel.DataAnnotations; + using IDisposableGenerator; + + [GenerateDispose(false)] + [WithoutThrowIfDisposed] + internal partial class TestDisposable + { + [NullOnDispose] + [StringLength(50)] + public string? test { get; set; } = "stuff here."; + } + } + + """; + + await RunTest(generatedSource, testSource, LanguageVersion.CSharp9); + } } diff --git a/tests/IDisposableGeneratorTests.VisualBasic.cs b/tests/IDisposableGeneratorTests.VisualBasic.cs index f44036c..c9b9a4a 100644 --- a/tests/IDisposableGeneratorTests.VisualBasic.cs +++ b/tests/IDisposableGeneratorTests.VisualBasic.cs @@ -29,6 +29,12 @@ If Not Me.isDisposed AndAlso disposing Then Me.isDisposed = True End If End Sub + + Friend Sub ThrowIfDisposed() + If Me.isDisposed Then + Throw New ObjectDisposedException(NameOf(TestDisposable)) + End If + End Sub End Class End Namespace ", @"Imports System @@ -74,6 +80,12 @@ If Not Me.isDisposed AndAlso disposing Then Me.isDisposed = True End If End Sub + + Friend Sub ThrowIfDisposed() + If Me.isDisposed Then + Throw New ObjectDisposedException(NameOf(TestDisposable)) + End If + End Sub End Class End Namespace ", @"Imports System @@ -123,6 +135,12 @@ End If Me.isDisposed = True End If End Sub + + Friend Sub ThrowIfDisposed() + If Me.isDisposed Then + Throw New ObjectDisposedException(NameOf(TestDisposable)) + End If + End Sub End Class End Namespace ", @"Imports System @@ -163,6 +181,12 @@ End If ' On Streams call MyBase.Dispose(disposing)!!! MyBase.Dispose(disposing) End Sub + + Friend Sub ThrowIfDisposed() + If Me.isDisposed Then + Throw New ObjectDisposedException(NameOf(TestDisposable)) + End If + End Sub End Class End Namespace ", @"Imports System @@ -254,6 +278,12 @@ End If ' On Streams call MyBase.Dispose(disposing)!!! MyBase.Dispose(disposing) End Sub + + Friend Sub ThrowIfDisposed() + If Me.isDisposed Then + Throw New ObjectDisposedException(NameOf(TestDisposable)) + End If + End Sub End Class End Namespace ", @"Imports System @@ -347,6 +377,12 @@ If Not Me.isDisposed AndAlso disposing Then Me.isDisposed = True End If End Sub + + Friend Sub ThrowIfDisposed() + If Me.isDisposed Then + Throw New ObjectDisposedException(NameOf(TestDisposable)) + End If + End Sub End Class End Namespace ", @"Imports System @@ -395,6 +431,12 @@ If Not Me.isDisposed AndAlso disposing Then Me.isDisposed = True End If End Sub + + Friend Sub ThrowIfDisposed() + If Me.isDisposed Then + Throw New ObjectDisposedException(NameOf(TestDisposable)) + End If + End Sub End Class End Namespace ", @"Imports System @@ -411,4 +453,56 @@ Friend Partial Class TestDisposable End Class End Namespace ", null); + + [Fact] + public async Task TestWithoutThrowIfDisposedVisualBasic() + { + const string generatedSource = """ + ' + Imports System + + Namespace MyApp + + Friend Partial Class TestDisposable + Implements IDisposable + + Private isDisposed As Boolean + + ''' + ''' Cleans up the resources used by . + ''' + Public Sub Dispose() Implements IDisposable.Dispose + Me.Dispose(True) + End Sub + + Private Sub Dispose(ByVal disposing As Boolean) + If Not Me.isDisposed AndAlso disposing Then + Me.test = Nothing + Me.isDisposed = True + End If + End Sub + End Class + End Namespace + + """; + + const string testSource = """ + Imports System + Imports System.ComponentModel.DataAnnotations + Imports IDisposableGenerator + + Namespace MyApp + + + Friend Partial Class TestDisposable + + + Public Property test As String = "stuff here." + End Class + End Namespace + + """; + + await RunTest(generatedSource, testSource, null); + } }