diff --git a/src/IDisposableGenerator/ClassItems.cs b/src/IDisposableGenerator/ClassItems.cs index 4d19549..777bc95 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 ThrowIfDisposed { 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($", ThrowIfDisposed: {this.ThrowIfDisposed}") .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..4bacadf 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.ThrowIfDisposed) + { + _ = 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.ThrowIfDisposed) + { + _ = 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.ThrowIfDisposed) + { + _ = 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..284c1f7 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,17 @@ namespace IDisposableGenerator { } } + + // used only by a source generator to generate Dispose() and Dispose(bool). + [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + internal class GenerateThrowIfDisposedAttribute : Attribute + { + } } -#pragma warning restore SA1636, 8618 - - - - ' <autogenerated/> +#pragma warning restore SA1636, 8618 + + + ' <autogenerated/> #Disable Warning SA1636 Imports System @@ -101,8 +205,13 @@ 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 GenerateThrowIfDisposedAttribute + Inherits Attribute + End Class + End Namespace -#Enable Warning SA1636 - - +#Enable Warning SA1636 + \ No newline at end of file diff --git a/src/IDisposableGenerator/WorkItemCollection.cs b/src/IDisposableGenerator/WorkItemCollection.cs index 9530cb6..4402a81 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!, - }; + 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 "GenerateThrowIfDisposedAttribute": + result.ThrowIfDisposed = true; + break; + default: + break; + } + } - return result; + return !hasDisposalGeneration ? null : result; } private static void CheckAttributesOnMember(ISymbol member, diff --git a/tests/IDisposableGeneratorTests.CSharp10.cs b/tests/IDisposableGeneratorTests.CSharp10.cs index e329199..5ab6039 100644 --- a/tests/IDisposableGeneratorTests.CSharp10.cs +++ b/tests/IDisposableGeneratorTests.CSharp10.cs @@ -385,6 +385,7 @@ private void Dispose(bool disposing) } "} }; + await RunTest(@"// namespace MyApp; @@ -420,4 +421,61 @@ internal partial class TestDisposable } ", LanguageVersion.CSharp10, testSources, generatedSources); } + + + [Fact] + public async Task TestGenerateThrowIfDisposedCSharp10() + { + 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; + } + } + + internal void ThrowIfDisposed() + { + if (this.isDisposed) + { + throw new ObjectDisposedException(nameof(TestDisposable)); + } + } + } + + """; + + const string testSource = """ + global using System; + global using System.ComponentModel.DataAnnotations; + global using IDisposableGenerator; + + namespace MyApp; + + [GenerateDispose(false)] + [GenerateThrowIfDisposed] + 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..2e633b7 100644 --- a/tests/IDisposableGeneratorTests.CSharp9.cs +++ b/tests/IDisposableGeneratorTests.CSharp9.cs @@ -353,4 +353,65 @@ internal partial class TestDisposable } } "); + + [Fact] + public async Task TestGenerateThrowIfDisposedCSharp9() + { + 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; + } + } + + internal void ThrowIfDisposed() + { + if (this.isDisposed) + { + throw new ObjectDisposedException(nameof(TestDisposable)); + } + } + } + } + + """; + + const string testSource = """ + namespace MyApp + { + using System; + using System.ComponentModel.DataAnnotations; + using IDisposableGenerator; + + [GenerateDispose(false)] + [GenerateThrowIfDisposed] + 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..2cc67ca 100644 --- a/tests/IDisposableGeneratorTests.VisualBasic.cs +++ b/tests/IDisposableGeneratorTests.VisualBasic.cs @@ -411,4 +411,62 @@ Friend Partial Class TestDisposable End Class End Namespace ", null); + + [Fact] + public async Task TestGenerateThrowIfDisposedVisualBasic() + { + 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 + + Friend Sub ThrowIfDisposed() + If Me.isDisposed Then + Throw New ObjectDisposedException(NameOf(TestDisposable)) + 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); + } }