diff --git a/src/IDisposableGenerator.CSharp/IDisposableGenerator.cs b/src/IDisposableGenerator.CSharp/IDisposableGenerator.cs index 949f737..6d8f0a7 100644 --- a/src/IDisposableGenerator.CSharp/IDisposableGenerator.cs +++ b/src/IDisposableGenerator.CSharp/IDisposableGenerator.cs @@ -1,5 +1,7 @@ namespace IDisposableGenerator; +using System.Text; + [Generator] public class IDisposableGenerator : IIncrementalGenerator { @@ -45,7 +47,8 @@ public void Initialize(IncrementalGeneratorInitializationContext context) context.RegisterPostInitializationOutput(ctx => { // Always generate the attributes. - var attributeSource = Properties.Resources.AttributeCodeCSharp!; + var attributeSource = new StringBuilder(); + _ = attributeSource.Append(Properties.Resources.AttributeCodeCSharp!); attributeSource.ToSourceFile("GeneratedAttributes.g.cs", ref ctx); }); } diff --git a/src/IDisposableGenerator.VisualBasic/IDisposableGenerator.cs b/src/IDisposableGenerator.VisualBasic/IDisposableGenerator.cs index e5d5383..015135b 100644 --- a/src/IDisposableGenerator.VisualBasic/IDisposableGenerator.cs +++ b/src/IDisposableGenerator.VisualBasic/IDisposableGenerator.cs @@ -1,5 +1,7 @@ namespace IDisposableGenerator; +using System.Text; + [Generator(LanguageNames.VisualBasic)] public class IDisposableGeneratorVB : IIncrementalGenerator { @@ -28,7 +30,8 @@ public void Initialize(IncrementalGeneratorInitializationContext context) context.RegisterPostInitializationOutput(ctx => { // Always generate the attributes. - var attributeSource = Properties.Resources.AttributeCodeVisualBasic!; + var attributeSource = new StringBuilder(); + _ = attributeSource.Append(Properties.Resources.AttributeCodeVisualBasic!); attributeSource.ToSourceFile("GeneratedAttributes.g.vb", ref ctx); }); } diff --git a/src/IDisposableGenerator/ClassItems.cs b/src/IDisposableGenerator/ClassItems.cs index a21c35f..51f7f1d 100644 --- a/src/IDisposableGenerator/ClassItems.cs +++ b/src/IDisposableGenerator/ClassItems.cs @@ -6,14 +6,22 @@ internal class ClassItems 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 Owns { get; } = []; + public List Fields { get; } = []; public List SetNull { get; } = []; public List Methods { get; } = []; + public static bool IsReadOnlyField(ISymbol member) + => (member is IFieldSymbol fieldSymbol && fieldSymbol.IsReadOnly) || + (member is IPropertySymbol propertySymbol && propertySymbol.IsReadOnly); + public bool AddSetNull(ISymbol member) { - this.SetNull.Add(member.Name); + if (!IsReadOnlyField(member) || member is IEventSymbol) + { + this.SetNull.Add(member.Name); + } + return true; } @@ -27,11 +35,11 @@ public bool AddField(TypedConstant arg, ISymbol member) { if ((bool)arg.Value!) { - this.Owns.Add(member.Name); + this.Owns.Add(member); } else { - this.Fields.Add(member.Name); + this.Fields.Add(member); } return true; @@ -54,4 +62,340 @@ public override string ToString() .Append($", Methods Count: {this.Methods.Count}"); return result.ToString(); } + + public string ToCSharp9CodeString() + { + var result = new StringBuilder(); + _ = result.Append($@" + {(this.Accessibility == Accessibility.Public ? "public" : "internal")} partial class {this.Name}{(!this.Stream ? " : IDisposable" : "")} + {{ + private bool isDisposed; +"); + if (this.Owns.Count is not 0) + { + _ = result.Append(this.Stream ? @" + internal bool KeepOpen { get; } +" : @" + internal bool IsOwned { get; set; } +"); + } + + _ = result.Append($@" + {(!this.Stream ? $@"/// + /// Cleans up the resources used by . + /// + public void Dispose() + {{ + this.Dispose(true); + GC.SuppressFinalize(this); + }} + + private" : @"/// + protected override")} void Dispose(bool disposing) + {{ + if (!this.isDisposed && disposing) + {{ +"); + if (this.Methods.Count is not 0) + { + foreach (var methodItem in this.Methods) + { + _ = result.Append($@" this.{methodItem}(); +"); + } + } + + if (this.Owns.Count is not 0) + { + _ = result.Append($@" if ({(this.Stream ? "!this.KeepOpen" : "this.IsOwned")}) + {{ +"); + foreach (var ownedItem in this.Owns) + { + // automatically set to null after Dispose(). + _ = result.Append($@" this.{ownedItem.Name}?.Dispose(); +{(!IsReadOnlyField(ownedItem) ? $@" this.{ownedItem.Name} = null; +" : string.Empty)}"); + } + + _ = result.Append(@" } +"); + } + + if (this.Fields.Count is not 0) + { + foreach (var fieldItem in this.Fields) + { + // automatically set to null after Dispose(). + _ = result.Append($@" this.{fieldItem.Name}?.Dispose(); +{(!IsReadOnlyField(fieldItem) ? $@" this.{fieldItem.Name} = null; +" : string.Empty)}"); + } + } + + if (this.SetNull.Count is not 0) + { + foreach (var nullItem in this.SetNull) + { + _ = result.Append($@" this.{nullItem} = null; +"); + } + } + + _ = result.Append(@" this.isDisposed = true; + } +"); + if (this.Stream) + { + _ = result.Append(@" + // On Streams call base.Dispose(disposing)!!! + base.Dispose(disposing); +"); + } + + _ = result.Append(@" } +"); + if (!this.WithoutThrowIfDisposed) + { + _ = result.Append($@" + internal void ThrowIfDisposed() + {{ + if (this.isDisposed) + {{ + throw new ObjectDisposedException(nameof({this.Name})); + }} + }} +"); + } + + _ = result.Append(@" } +"); + + return result.ToString(); + } + + public string ToCSharp10CodeString() + { + var result = new StringBuilder(); + _ = result.Append($@" +{(this.Accessibility == Accessibility.Public ? "public" : "internal")} partial class {this.Name}{(!this.Stream ? " : IDisposable" : "")} +{{ + private bool isDisposed; +"); + if (this.Owns.Count is not 0) + { + _ = result.Append(this.Stream ? @" + internal bool KeepOpen { get; } +" : @" + internal bool IsOwned { get; set; } +"); + } + + _ = result.Append($@" + {(!this.Stream ? $@"/// + /// Cleans up the resources used by . + /// + public void Dispose() + {{ + this.Dispose(true); + GC.SuppressFinalize(this); + }} + + private" : @"/// + protected override")} void Dispose(bool disposing) + {{ + if (!this.isDisposed && disposing) + {{ +"); + if (this.Methods.Count is not 0) + { + foreach (var methodItem in this.Methods) + { + _ = result.Append($@" this.{methodItem}(); +"); + } + } + + if (this.Owns.Count is not 0) + { + _ = result.Append($@" if ({(this.Stream ? "!this.KeepOpen" : "this.IsOwned")}) + {{ +"); + foreach (var ownedItem in this.Owns) + { + // automatically set to null after Dispose(). + _ = result.Append($@" this.{ownedItem.Name}?.Dispose(); +{(!IsReadOnlyField(ownedItem) ? $@" this.{ownedItem.Name} = null; +" : string.Empty)}"); + } + + _ = result.Append(@" } +"); + } + + if (this.Fields.Count is not 0) + { + foreach (var fieldItem in this.Fields) + { + // automatically set to null after Dispose(). + _ = result.Append($@" this.{fieldItem.Name}?.Dispose(); +{(!IsReadOnlyField(fieldItem) ? $@" this.{fieldItem.Name} = null; +" : string.Empty)}"); + } + } + + if (this.SetNull.Count is not 0) + { + foreach (var nullItem in this.SetNull) + { + _ = result.Append($@" this.{nullItem} = null; +"); + } + } + + _ = result.Append(@" this.isDisposed = true; + } +"); + if (this.Stream) + { + _ = result.Append(@" + // On Streams call base.Dispose(disposing)!!! + base.Dispose(disposing); +"); + } + + _ = result.Append(""" + } + + """); + + if (!this.WithoutThrowIfDisposed) + { + _ = result.Append($@" + internal void ThrowIfDisposed() + {{ + if (this.isDisposed) + {{ + throw new ObjectDisposedException(nameof({this.Name})); + }} + }} +"); + } + + _ = result.Append(@"} +"); + return result.ToString(); + } + + public string ToVisualBasicCodeString() + { + var result = new StringBuilder(); + _ = result.Append($@" + {(this.Accessibility == Accessibility.Public ? "Public" : "Friend")} Partial Class {this.Name}{(!this.Stream ? @" + Implements IDisposable +" : "")} + Private isDisposed As Boolean +"); + if (this.Owns.Count is not 0) + { + _ = result.Append(this.Stream ? @" + Friend ReadOnly Property KeepOpen As Boolean +" : @" + Friend Property IsOwned As Boolean +"); + } + + _ = result.Append($@" + {(!this.Stream ? $@"''' + ''' Cleans up the resources used by . + ''' + Public Sub Dispose() Implements IDisposable.Dispose + Me.Dispose(True) + GC.SuppressFinalize(Me) + End Sub + + Private" : @"''' + Protected Overrides")} Sub Dispose(ByVal disposing As Boolean) + If Not Me.isDisposed AndAlso disposing Then +"); + if (this.Methods.Count is not 0) + { + foreach (var methodItem in this.Methods) + { + _ = result.Append($@" Me.{methodItem}() +"); + } + } + + if (this.Owns.Count is not 0) + { + _ = result.Append($@" If {(this.Stream ? "Not Me.KeepOpen" : "Me.IsOwned")} Then +"); + foreach (var ownedItem in this.Owns) + { + // automatically set to null after Dispose(). + _ = result.Append($@" Me.{ownedItem.Name}?.Dispose() +{(!IsReadOnlyField(ownedItem) ? $@" Me.{ownedItem.Name} = Nothing +" : string.Empty)}"); + } + + _ = result.Append(@" End If +"); + } + + if (this.Fields.Count is not 0) + { + foreach (var fieldItem in this.Fields) + { + // automatically set to null after Dispose(). + _ = result.Append($@" Me.{fieldItem.Name}?.Dispose() +{(!IsReadOnlyField(fieldItem) ? $@" Me.{fieldItem.Name} = Nothing +" : string.Empty)}"); + } + } + + if (this.SetNull.Count is not 0) + { + foreach (var nullItem in this.SetNull) + { + _ = result.Append($@" Me.{nullItem} = Nothing +"); + } + } + + _ = result.Append(@" Me.isDisposed = True + End If +"); + if (this.Stream) + { + _ = result.Append(@" + ' On Streams call MyBase.Dispose(disposing)!!! + MyBase.Dispose(disposing) +"); + } + + _ = result.Append(""" + End Sub + + """); + + if (!this.WithoutThrowIfDisposed) + { + _ = result.Append($$""" + + Friend Sub ThrowIfDisposed() + If Me.isDisposed Then + Throw New ObjectDisposedException(NameOf({{this.Name}})) + End If + End Sub + + """); + } + + _ = result.Append(""" + End Class + + """); + return result.ToString(); + } } diff --git a/src/IDisposableGenerator/DisposableCodeWriter.cs b/src/IDisposableGenerator/DisposableCodeWriter.cs index 1615f1c..931a196 100644 --- a/src/IDisposableGenerator/DisposableCodeWriter.cs +++ b/src/IDisposableGenerator/DisposableCodeWriter.cs @@ -12,125 +12,11 @@ Imports System "); foreach (var workItem in workItemCollection.GetWorkItems()) { - _ = sourceBuilder.Append($@" -Namespace {workItem.Namespace} -"); - foreach (var classItem in workItem.Classes) - { - _ = sourceBuilder.Append($@" - {(classItem.Accessibility == Accessibility.Public ? "Public" : "Friend")} Partial Class {classItem.Name}{(!classItem.Stream ? @" - Implements IDisposable -" : "")} - Private isDisposed As Boolean -"); - - if (classItem.Owns.Count is not 0) - { - _ = sourceBuilder.Append(classItem.Stream ? @" - Friend ReadOnly Property KeepOpen As Boolean -" : @" - Friend Property IsOwned As Boolean -"); - } - - _ = sourceBuilder.Append($@" - {(!classItem.Stream ? $@"''' - ''' Cleans up the resources used by . - ''' - Public Sub Dispose() Implements IDisposable.Dispose - Me.Dispose(True) - End Sub - - Private" : @"''' - Protected Overrides")} Sub Dispose(ByVal disposing As Boolean) - If Not Me.isDisposed AndAlso disposing Then -"); - if (classItem.Methods.Count is not 0) - { - foreach (var methodItem in classItem.Methods) - { - _ = sourceBuilder.Append($@" Me.{methodItem}() -"); - } - } - - if (classItem.Owns.Count is not 0) - { - _ = sourceBuilder.Append($@" If {(classItem.Stream ? "Not Me.KeepOpen" : "Me.IsOwned")} Then -"); - foreach (var ownedItem in classItem.Owns) - { - // automatically set to null after Dispose(). - _ = sourceBuilder.Append($@" Me.{ownedItem}?.Dispose() - Me.{ownedItem} = Nothing -"); - } - - _ = sourceBuilder.Append(@" End If -"); - } - - if (classItem.Fields.Count is not 0) - { - foreach (var fieldItem in classItem.Fields) - { - // automatically set to null after Dispose(). - _ = sourceBuilder.Append($@" Me.{fieldItem}?.Dispose() - Me.{fieldItem} = Nothing -"); - } - } - - if (classItem.SetNull.Count is not 0) - { - foreach (var nullItem in classItem.SetNull) - { - _ = sourceBuilder.Append($@" Me.{nullItem} = Nothing -"); - } - } - - _ = sourceBuilder.Append(@" Me.isDisposed = True - End If -"); - if (classItem.Stream) - { - _ = sourceBuilder.Append(@" - ' On Streams call MyBase.Dispose(disposing)!!! - MyBase.Dispose(disposing) -"); - } - - _ = 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 -"); + _ = sourceBuilder.Append(workItem.ToVisualBasicCodeString()); } // inject the created sources into the users compilation. - sourceBuilder.ToString().ToSourceFile("Disposables.g.vb", ref context); + sourceBuilder.ToSourceFile("Disposables.g.vb", ref context); } public static void WriteDisposableCodeCSharp10( @@ -139,123 +25,10 @@ public static void WriteDisposableCodeCSharp10( { foreach (var workItem in workItemCollection.GetWorkItems()) { - StringBuilder sourceBuilder = new("// "); - _ = sourceBuilder.Append($@" -namespace {workItem.Namespace}; -"); - foreach (var classItem in workItem.Classes) - { - _ = sourceBuilder.Append($@" -{(classItem.Accessibility == Accessibility.Public ? "public" : "internal")} partial class {classItem.Name}{(!classItem.Stream ? " : IDisposable" : "")} -{{ - private bool isDisposed; -"); - if (classItem.Owns.Count is not 0) - { - _ = sourceBuilder.Append(classItem.Stream ? @" - internal bool KeepOpen { get; } -" : @" - internal bool IsOwned { get; set; } -"); - } - - _ = sourceBuilder.Append($@" - {(!classItem.Stream ? $@"/// - /// Cleans up the resources used by . - /// - public void Dispose() => this.Dispose(true); - - private" : @"/// - protected override")} void Dispose(bool disposing) - {{ - if (!this.isDisposed && disposing) - {{ -"); - if (classItem.Methods.Count is not 0) - { - foreach (var methodItem in classItem.Methods) - { - _ = sourceBuilder.Append($@" this.{methodItem}(); -"); - } - } - - if (classItem.Owns.Count is not 0) - { - _ = sourceBuilder.Append($@" if ({(classItem.Stream ? "!this.KeepOpen" : "this.IsOwned")}) - {{ -"); - foreach (var ownedItem in classItem.Owns) - { - // automatically set to null after Dispose(). - _ = sourceBuilder.Append($@" this.{ownedItem}?.Dispose(); - this.{ownedItem} = null; -"); - } - - _ = sourceBuilder.Append(@" } -"); - } - - if (classItem.Fields.Count is not 0) - { - foreach (var fieldItem in classItem.Fields) - { - // automatically set to null after Dispose(). - _ = sourceBuilder.Append($@" this.{fieldItem}?.Dispose(); - this.{fieldItem} = null; -"); - } - } - - if (classItem.SetNull.Count is not 0) - { - foreach (var nullItem in classItem.SetNull) - { - _ = sourceBuilder.Append($@" this.{nullItem} = null; -"); - } - } - - _ = sourceBuilder.Append(@" this.isDisposed = true; - } -"); - if (classItem.Stream) - { - _ = sourceBuilder.Append(@" - // On Streams call base.Dispose(disposing)!!! - base.Dispose(disposing); -"); - } - - _ = sourceBuilder.Append(""" - } - - """); - - if (!classItem.WithoutThrowIfDisposed) - { - _ = sourceBuilder.Append($$""" - - internal void ThrowIfDisposed() - { - if (this.isDisposed) - { - throw new ObjectDisposedException(nameof({{classItem.Name}})); - } - } - - """); - } - - _ = sourceBuilder.Append(""" - } - - """); - } + var sourceBuilder = workItem.ToCSharp10CodeString(); // inject the created sources into the users compilation. - sourceBuilder.ToString().ToSourceFile($@"Disposables{( + sourceBuilder.ToSourceFile($@"Disposables{( workItemCollection.Count > 1 ? $".{workItemCollection.IndexOf(workItem)}" : string.Empty)}.g.cs", ref context); @@ -269,129 +42,10 @@ public static void WriteDisposableCodeCSharp9( StringBuilder sourceBuilder = new("// "); foreach (var workItem in workItemCollection.GetWorkItems()) { - _ = sourceBuilder.Append($@" -namespace {workItem.Namespace} -{{ - using global::System; -"); - foreach (var classItem in workItem.Classes) - { - _ = sourceBuilder.Append($@" - {(classItem.Accessibility == Accessibility.Public ? "public" : "internal")} partial class {classItem.Name}{(!classItem.Stream ? " : IDisposable" : "")} - {{ - private bool isDisposed; -"); - if (classItem.Owns.Count is not 0) - { - _ = sourceBuilder.Append(classItem.Stream ? @" - internal bool KeepOpen { get; } -" : @" - internal bool IsOwned { get; set; } -"); - } - - _ = sourceBuilder.Append($@" - {(!classItem.Stream ? $@"/// - /// Cleans up the resources used by . - /// - public void Dispose() => this.Dispose(true); - - private" : @"/// - protected override")} void Dispose(bool disposing) - {{ - if (!this.isDisposed && disposing) - {{ -"); - if (classItem.Methods.Count is not 0) - { - foreach (var methodItem in classItem.Methods) - { - _ = sourceBuilder.Append($@" this.{methodItem}(); -"); - } - } - - if (classItem.Owns.Count is not 0) - { - _ = sourceBuilder.Append($@" if ({(classItem.Stream ? "!this.KeepOpen" : "this.IsOwned")}) - {{ -"); - foreach (var ownedItem in classItem.Owns) - { - // automatically set to null after Dispose(). - _ = sourceBuilder.Append($@" this.{ownedItem}?.Dispose(); - this.{ownedItem} = null; -"); - } - - _ = sourceBuilder.Append(@" } -"); - } - - if (classItem.Fields.Count is not 0) - { - foreach (var fieldItem in classItem.Fields) - { - // automatically set to null after Dispose(). - _ = sourceBuilder.Append($@" this.{fieldItem}?.Dispose(); - this.{fieldItem} = null; -"); - } - } - - if (classItem.SetNull.Count is not 0) - { - foreach (var nullItem in classItem.SetNull) - { - _ = sourceBuilder.Append($@" this.{nullItem} = null; -"); - } - } - - _ = sourceBuilder.Append(@" this.isDisposed = true; - } -"); - if (classItem.Stream) - { - _ = sourceBuilder.Append(@" - // On Streams call base.Dispose(disposing)!!! - base.Dispose(disposing); -"); - } - - _ = 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(workItem.ToCSharp9CodeString()); } // inject the created source into the users compilation. - sourceBuilder.ToString().ToSourceFile("Disposables.g.cs", ref context); + sourceBuilder.ToSourceFile("Disposables.g.cs", ref context); } } diff --git a/src/IDisposableGenerator/SemanticHelper.cs b/src/IDisposableGenerator/SemanticHelper.cs index 6f79f62..c1ea117 100644 --- a/src/IDisposableGenerator/SemanticHelper.cs +++ b/src/IDisposableGenerator/SemanticHelper.cs @@ -16,21 +16,21 @@ public static string FullNamespace(this ISymbol symbol) iterator = iterator.ContainingNamespace; } - return string.Join(".", parts); + return parts.Count == 0 ? string.Empty : string.Join(".", parts); } public static bool FullNamespaceEquals(this ISymbol symbol, string @namespace) => symbol.FullNamespace().Equals(@namespace, StringComparison.Ordinal); public static void ToSourceFile( - this string source, + this StringBuilder source, string sourceName, ref SourceProductionContext context) - => context.AddSource(sourceName, source); + => context.AddSource(sourceName, source.ToString()); public static void ToSourceFile( - this string source, + this StringBuilder source, string sourceName, ref IncrementalGeneratorPostInitializationContext context) - => context.AddSource(sourceName, source); + => context.AddSource(sourceName, source.ToString()); } diff --git a/src/IDisposableGenerator/WorkItem.cs b/src/IDisposableGenerator/WorkItem.cs index 4d2a955..8682649 100644 --- a/src/IDisposableGenerator/WorkItem.cs +++ b/src/IDisposableGenerator/WorkItem.cs @@ -1,5 +1,7 @@ namespace IDisposableGenerator; +using System; + internal class WorkItem { public string Namespace { get; set; } = null!; @@ -21,4 +23,98 @@ public override string ToString() return sb.ToString(); } + + public string ToCSharp9CodeString() + { + var result = new StringBuilder(); + _ = result.Append($@" +{(!string.IsNullOrEmpty(this.Namespace) ? $@"namespace {this.Namespace} +{{ + " : string.Empty)}using global::System; +"); + foreach (var classItem in this.Classes) + { + var code = classItem.ToCSharp9CodeString(); + if (string.IsNullOrEmpty(this.Namespace)) + { + code = ReduceIndentation(code); + } + + _ = result.Append(code); + } + + if (!string.IsNullOrEmpty(this.Namespace)) + { + _ = result.Append(@"} +"); + } + + return result.ToString(); + } + + public StringBuilder ToCSharp10CodeString() + { + var result = new StringBuilder("// "); + _ = result.Append($@" +{(!string.IsNullOrEmpty(this.Namespace) ? $@"namespace {this.Namespace}; +" : string.Empty)}"); + foreach (var classItem in this.Classes) + { + _ = result.Append(classItem.ToCSharp10CodeString()); + } + + return result; + } + + public string ToVisualBasicCodeString() + { + var result = new StringBuilder(); + if (!string.IsNullOrEmpty(this.Namespace)) + { + _ = result.Append($@" +Namespace {this.Namespace} +"); + } + + foreach (var classItem in this.Classes) + { + var code = classItem.ToVisualBasicCodeString(); + if (string.IsNullOrEmpty(this.Namespace)) + { + code = ReduceIndentation(code); + } + + _ = result.Append(code); + } + + if (!string.IsNullOrEmpty(this.Namespace)) + { + _ = result.Append(@"End Namespace +"); + } + + return result.ToString(); + } + + private static string ReduceIndentation(string code) + { + var eol = (code.Contains("\r\n"), code.Contains("\n")) switch + { + (true, true) => "\r\n", + (false, true) => "\n", + (false, false) => "\r", + _ => throw new InvalidOperationException("bug"), + }; + + var lines = code.Split([eol], StringSplitOptions.None); + for (var i = 0; i < lines.Length; i++) + { + if (lines[i].StartsWith(" ", StringComparison.Ordinal)) + { + lines[i] = lines[i].Substring(4); + } + } + + return string.Join(eol, lines); + } } diff --git a/tests/CSGeneratorTest.cs b/tests/CSGeneratorTest.cs index 3f247e1..78d2cff 100644 --- a/tests/CSGeneratorTest.cs +++ b/tests/CSGeneratorTest.cs @@ -1,5 +1,5 @@ namespace IDisposableGenerator.Tests; -public class CSGeneratorTest : CSharpIncrementalGeneratorTest +public class CSGeneratorTest : CSharpIncrementalGeneratorTest { } diff --git a/tests/Directory.Build.targets b/tests/Directory.Build.targets index 9777dbf..a03e475 100644 --- a/tests/Directory.Build.targets +++ b/tests/Directory.Build.targets @@ -11,7 +11,6 @@ - diff --git a/tests/Directory.Packages.props b/tests/Directory.Packages.props index 3c89b32..08aab3a 100644 --- a/tests/Directory.Packages.props +++ b/tests/Directory.Packages.props @@ -8,8 +8,8 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/IDisposableGeneratorTests.CSharp10.cs b/tests/IDisposableGeneratorTests.CSharp10.cs index 3145737..b969352 100644 --- a/tests/IDisposableGeneratorTests.CSharp10.cs +++ b/tests/IDisposableGeneratorTests.CSharp10.cs @@ -14,7 +14,11 @@ public partial class TestDisposable : IDisposable /// /// Cleans up the resources used by . /// - public void Dispose() => this.Dispose(true); + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } private void Dispose(bool disposing) { @@ -63,7 +67,11 @@ internal partial class TestDisposable : IDisposable /// /// Cleans up the resources used by . /// - public void Dispose() => this.Dispose(true); + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } private void Dispose(bool disposing) { @@ -114,7 +122,11 @@ internal partial class TestDisposable : IDisposable /// /// Cleans up the resources used by . /// - public void Dispose() => this.Dispose(true); + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } private void Dispose(bool disposing) { @@ -291,7 +303,11 @@ internal partial class TestDisposable : IDisposable /// /// Cleans up the resources used by . /// - public void Dispose() => this.Dispose(true); + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } private void Dispose(bool disposing) { @@ -348,7 +364,11 @@ internal partial class TestDisposable : IDisposable /// /// Cleans up the resources used by . /// - public void Dispose() => this.Dispose(true); + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } private void Dispose(bool disposing) { @@ -407,7 +427,11 @@ internal partial class TestDisposable : IDisposable /// /// Cleans up the resources used by . /// - public void Dispose() => this.Dispose(true); + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } private void Dispose(bool disposing) { @@ -437,7 +461,11 @@ internal partial class AnotherDisposable : IDisposable /// /// Cleans up the resources used by . /// - public void Dispose() => this.Dispose(true); + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } private void Dispose(bool disposing) { @@ -516,7 +544,11 @@ internal partial class TestDisposable : IDisposable /// /// Cleans up the resources used by . /// - public void Dispose() => this.Dispose(true); + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } private void Dispose(bool disposing) { @@ -549,4 +581,131 @@ internal partial class TestDisposable await RunTest(generatedSource, testSource, LanguageVersion.CSharp10); } + + [Fact] + public async Task TestGeneratingDisposableWithReadonlyFieldsCSharp10() + { + const string generatedSource = """ + // + namespace Test; + + public partial class LogWriter : IDisposable + { + private bool isDisposed; + + /// + /// Cleans up the resources used by . + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (!this.isDisposed && disposing) + { + this._streamWriter?.Dispose(); + this.isDisposed = true; + } + } + + internal void ThrowIfDisposed() + { + if (this.isDisposed) + { + throw new ObjectDisposedException(nameof(LogWriter)); + } + } + } + + """; + + const string testSource = """ + global using System; + global using System.IO; + global using IDisposableGenerator; + + namespace Test; + + [GenerateDispose(false)] + public partial class LogWriter + { + [DisposeField(false)] + private readonly StreamWriter _streamWriter; + + public LogWriter(string path) + { + _streamWriter = new StreamWriter(path); + } + + public void WriteLine(string text) => _streamWriter.WriteLine(text.ToUpper()); + } + """; + + await RunTest(generatedSource, testSource, LanguageVersion.CSharp10); + } + + [Fact] + public async Task TestGeneratingDisposableWithoutNamespaceCSharp10() + { + const string generatedSource = """ + // + + public partial class LogWriter : IDisposable + { + private bool isDisposed; + + /// + /// Cleans up the resources used by . + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (!this.isDisposed && disposing) + { + this._streamWriter?.Dispose(); + this.isDisposed = true; + } + } + + internal void ThrowIfDisposed() + { + if (this.isDisposed) + { + throw new ObjectDisposedException(nameof(LogWriter)); + } + } + } + + """; + + const string testSource = """ + global using System; + global using System.IO; + global using IDisposableGenerator; + + [GenerateDispose(false)] + public partial class LogWriter + { + [DisposeField(false)] + private readonly StreamWriter _streamWriter; + + public LogWriter(string path) + { + _streamWriter = new StreamWriter(path); + } + + public void WriteLine(string text) => _streamWriter.WriteLine(text.ToUpper()); + } + """; + + await RunTest(generatedSource, testSource, LanguageVersion.CSharp10); + } } diff --git a/tests/IDisposableGeneratorTests.CSharp9.cs b/tests/IDisposableGeneratorTests.CSharp9.cs index 5633342..8525b2a 100644 --- a/tests/IDisposableGeneratorTests.CSharp9.cs +++ b/tests/IDisposableGeneratorTests.CSharp9.cs @@ -16,7 +16,11 @@ public partial class TestDisposable : IDisposable /// /// Cleans up the resources used by . /// - public void Dispose() => this.Dispose(true); + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } private void Dispose(bool disposing) { @@ -69,7 +73,11 @@ internal partial class TestDisposable : IDisposable /// /// Cleans up the resources used by . /// - public void Dispose() => this.Dispose(true); + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } private void Dispose(bool disposing) { @@ -124,7 +132,11 @@ internal partial class TestDisposable : IDisposable /// /// Cleans up the resources used by . /// - public void Dispose() => this.Dispose(true); + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } private void Dispose(bool disposing) { @@ -313,7 +325,11 @@ internal partial class TestDisposable : IDisposable /// /// Cleans up the resources used by . /// - public void Dispose() => this.Dispose(true); + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } private void Dispose(bool disposing) { @@ -374,7 +390,11 @@ internal partial class TestDisposable : IDisposable /// /// Cleans up the resources used by . /// - public void Dispose() => this.Dispose(true); + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } private void Dispose(bool disposing) { @@ -426,7 +446,11 @@ internal partial class TestDisposable : IDisposable /// /// Cleans up the resources used by . /// - public void Dispose() => this.Dispose(true); + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } private void Dispose(bool disposing) { @@ -462,4 +486,136 @@ internal partial class TestDisposable await RunTest(generatedSource, testSource, LanguageVersion.CSharp9); } + + [Fact] + public async Task TestGeneratingDisposableWithReadonlyFieldsCSharp9() + { + const string generatedSource = """ + // + namespace Test + { + using global::System; + + public partial class LogWriter : IDisposable + { + private bool isDisposed; + + /// + /// Cleans up the resources used by . + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (!this.isDisposed && disposing) + { + this._streamWriter?.Dispose(); + this.isDisposed = true; + } + } + + internal void ThrowIfDisposed() + { + if (this.isDisposed) + { + throw new ObjectDisposedException(nameof(LogWriter)); + } + } + } + } + + """; + + const string testSource = """ + using System; + using System.IO; + using IDisposableGenerator; + + namespace Test + { + [GenerateDispose(false)] + public partial class LogWriter + { + [DisposeField(false)] + private readonly StreamWriter _streamWriter; + + public LogWriter(string path) + { + _streamWriter = new StreamWriter(path); + } + + public void WriteLine(string text) => _streamWriter.WriteLine(text.ToUpper()); + } + } + """; + + await RunTest(generatedSource, testSource, LanguageVersion.CSharp9); + } + + [Fact] + public async Task TestGeneratingDisposableWithoutNamespaceCSharp9() + { + const string generatedSource = """ + // + using global::System; + + public partial class LogWriter : IDisposable + { + private bool isDisposed; + + /// + /// Cleans up the resources used by . + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (!this.isDisposed && disposing) + { + this._streamWriter?.Dispose(); + this.isDisposed = true; + } + } + + internal void ThrowIfDisposed() + { + if (this.isDisposed) + { + throw new ObjectDisposedException(nameof(LogWriter)); + } + } + } + + """; + + const string testSource = """ + using System; + using System.IO; + using IDisposableGenerator; + + [GenerateDispose(false)] + public partial class LogWriter + { + [DisposeField(false)] + private readonly StreamWriter _streamWriter; + + public LogWriter(string path) + { + _streamWriter = new StreamWriter(path); + } + + public void WriteLine(string text) => _streamWriter.WriteLine(text.ToUpper()); + } + """; + + await RunTest(generatedSource, testSource, LanguageVersion.CSharp9); + } } diff --git a/tests/IDisposableGeneratorTests.VisualBasic.cs b/tests/IDisposableGeneratorTests.VisualBasic.cs index c9b9a4a..7c932ba 100644 --- a/tests/IDisposableGeneratorTests.VisualBasic.cs +++ b/tests/IDisposableGeneratorTests.VisualBasic.cs @@ -19,6 +19,7 @@ Private isDisposed As Boolean ''' Public Sub Dispose() Implements IDisposable.Dispose Me.Dispose(True) + GC.SuppressFinalize(Me) End Sub Private Sub Dispose(ByVal disposing As Boolean) @@ -70,6 +71,7 @@ Private isDisposed As Boolean ''' Public Sub Dispose() Implements IDisposable.Dispose Me.Dispose(True) + GC.SuppressFinalize(Me) End Sub Private Sub Dispose(ByVal disposing As Boolean) @@ -123,6 +125,7 @@ Friend Property IsOwned As Boolean ''' Public Sub Dispose() Implements IDisposable.Dispose Me.Dispose(True) + GC.SuppressFinalize(Me) End Sub Private Sub Dispose(ByVal disposing As Boolean) @@ -366,6 +369,7 @@ Private isDisposed As Boolean ''' Public Sub Dispose() Implements IDisposable.Dispose Me.Dispose(True) + GC.SuppressFinalize(Me) End Sub Private Sub Dispose(ByVal disposing As Boolean) @@ -423,6 +427,7 @@ Private isDisposed As Boolean ''' Public Sub Dispose() Implements IDisposable.Dispose Me.Dispose(True) + GC.SuppressFinalize(Me) End Sub Private Sub Dispose(ByVal disposing As Boolean) @@ -473,6 +478,7 @@ Private isDisposed As Boolean ''' Public Sub Dispose() Implements IDisposable.Dispose Me.Dispose(True) + GC.SuppressFinalize(Me) End Sub Private Sub Dispose(ByVal disposing As Boolean) @@ -505,4 +511,127 @@ End Namespace await RunTest(generatedSource, testSource, null); } + + [Fact] + public async Task TestGeneratingDisposableWithReadonlyFieldsVisualBasic() + { + const string generatedSource = """ + ' + Imports System + + Namespace Test + + Public Partial Class LogWriter + Implements IDisposable + + Private isDisposed As Boolean + + ''' + ''' Cleans up the resources used by . + ''' + Public Sub Dispose() Implements IDisposable.Dispose + Me.Dispose(True) + GC.SuppressFinalize(Me) + End Sub + + Private Sub Dispose(ByVal disposing As Boolean) + If Not Me.isDisposed AndAlso disposing Then + Me._streamWriter?.Dispose() + Me.isDisposed = True + End If + End Sub + + Friend Sub ThrowIfDisposed() + If Me.isDisposed Then + Throw New ObjectDisposedException(NameOf(LogWriter)) + End If + End Sub + End Class + End Namespace + + """; + + const string testSource = """ + Imports System + Imports System.IO + Imports IDisposableGenerator + + Namespace Test + + Public Partial Class LogWriter + + Private ReadOnly _streamWriter As StreamWriter + + Public Sub New(ByVal path As String) + _streamWriter = New StreamWriter(path) + End Sub + + Public Sub WriteLine(ByVal text As String) + _streamWriter.WriteLine(text.ToUpper()) + End Sub + End Class + End Namespace + """; + + await RunTest(generatedSource, testSource, null); + } + + [Fact] + public async Task TestGeneratingDisposableWithoutNamespaceVisualBasic() + { + const string generatedSource = """ + ' + Imports System + + Public Partial Class LogWriter + Implements IDisposable + + Private isDisposed As Boolean + + ''' + ''' Cleans up the resources used by . + ''' + Public Sub Dispose() Implements IDisposable.Dispose + Me.Dispose(True) + GC.SuppressFinalize(Me) + End Sub + + Private Sub Dispose(ByVal disposing As Boolean) + If Not Me.isDisposed AndAlso disposing Then + Me._streamWriter?.Dispose() + Me.isDisposed = True + End If + End Sub + + Friend Sub ThrowIfDisposed() + If Me.isDisposed Then + Throw New ObjectDisposedException(NameOf(LogWriter)) + End If + End Sub + End Class + + """; + + const string testSource = """ + Imports System + Imports System.IO + Imports IDisposableGenerator + + + Public Partial Class LogWriter + + Private ReadOnly _streamWriter As StreamWriter + + Public Sub New(ByVal path As String) + _streamWriter = New StreamWriter(path) + End Sub + + Public Sub WriteLine(ByVal text As String) + _streamWriter.WriteLine(text.ToUpper()) + End Sub + End Class + """; + + await RunTest(generatedSource, testSource, null); + } } diff --git a/tests/IDisposableGeneratorTests.cs b/tests/IDisposableGeneratorTests.cs index e233e19..1065079 100644 --- a/tests/IDisposableGeneratorTests.cs +++ b/tests/IDisposableGeneratorTests.cs @@ -12,7 +12,7 @@ private static async Task RunTest( LanguageVersion? languageVersion = LanguageVersion.CSharp9, List? testSources = null, Dictionary? generatedSources = null) - where TestType : SourceGeneratorTest, IGeneratorTestBase, new() + where TestType : SourceGeneratorTest, IGeneratorTestBase, new() { var test = new TestType { diff --git a/tests/VBGeneratorTest.cs b/tests/VBGeneratorTest.cs index afc9221..ce04c6c 100644 --- a/tests/VBGeneratorTest.cs +++ b/tests/VBGeneratorTest.cs @@ -1,5 +1,5 @@ namespace IDisposableGenerator.Tests; -public class VBGeneratorTest : VisualBasicIncrementalGeneratorTest +public class VBGeneratorTest : VisualBasicIncrementalGeneratorTest { }