From fe00a0e43badfb52b3be50c2fd9051ad33926554 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Thu, 9 Apr 2026 15:17:04 +0200 Subject: [PATCH 1/4] feat: Addedd ReadOnlySpan, Refactor to class, improve XML docs - Refactored CSharpCodeBuilder and CodeBuilderBase from record to class, clarifying lack of thread-safety. - Added Append, AppendIf, AppendLine, and AppendLineIf overloads for ReadOnlySpan (and slices), with .NET Standard 2.0 compatibility. - Expanded and clarified XML documentation, especially for special character handling and collection-based doc methods. - Refined XML doc comment generation to use Append/AppendLine for consistency. - Renamed Intend() to Indent(); marked Intend() as obsolete. - Improved Clear() and indentation level management (no longer uses Interlocked). - Enhanced handling of closing braces/brackets in Append(string). - Updated .editorconfig with static readonly naming rules and other fixes. - Various minor code and documentation cleanups for clarity and consistency. --- .editorconfig | 25 +- .../CSharpCodeBuilder.Append.cs | 82 ++- .../CSharpCodeBuilder.AppendFormat.cs | 4 +- .../CSharpCodeBuilder.AppendIf.cs | 29 +- .../CSharpCodeBuilder.AppendLine.cs | 27 +- .../CSharpCodeBuilder.AppendLineIf.cs | 30 +- .../CSharpCodeBuilder.Clear.cs | 20 +- .../CSharpCodeBuilder.Documentation.cs | 100 ++- .../CSharpCodeBuilder.EnsureCapacity.cs | 4 +- .../CSharpCodeBuilder.Scope.cs | 10 +- .../CSharpCodeBuilder._EMPTY_.cs | 4 +- .../CSharpCodeBuilder.cs | 4 +- .../CodeBuilderBase.EnsureIndented.cs | 14 +- .../CodeBuilderBase.ToString.cs | 4 +- src/NetEvolve.CodeBuilder/CodeBuilderBase.cs | 10 +- ...uld_ProduceCorrectIndentation.verified.txt | 6 +- ...uld_ProduceCorrectIndentation.verified.txt | 6 +- ...eteClass_Should_MatchSnapshot.verified.txt | 2 +- ...s_Should_ProduceCorrectOutput.verified.txt | 4 +- ...n_Should_ProduceCorrectOutput.verified.txt | 2 +- ...s_Should_ProduceCorrectOutput.verified.txt | 2 +- ...t_Should_ProduceCorrectOutput.verified.txt | 3 +- ...e_Should_ProduceCorrectOutput.verified.txt | 5 +- ...tchSnapshots_02b3255120757173.verified.txt | 2 +- ...tchSnapshots_b6b4547aad375c78.verified.txt | 2 +- .../CSharpCodeBuilderTests.Append.cs | 74 ++- .../CSharpCodeBuilderTests.AppendFormat.cs | 192 +++++- .../CSharpCodeBuilderTests.AppendIf.cs | 60 ++ .../CSharpCodeBuilderTests.AppendLine.cs | 50 ++ .../CSharpCodeBuilderTests.AppendLineIf.cs | 60 ++ .../CSharpCodeBuilderTests.Clear.cs | 115 ++-- .../CSharpCodeBuilderTests.Documentation.cs | 626 +++++++++++------- .../CSharpCodeBuilderTests.Indentation.cs | 7 +- .../CSharpCodeBuilderTests.Scope.cs | 207 ++++-- 34 files changed, 1334 insertions(+), 458 deletions(-) diff --git a/.editorconfig b/.editorconfig index fa80a79..9c53899 100644 --- a/.editorconfig +++ b/.editorconfig @@ -87,10 +87,6 @@ insert_final_newline = false [*.sln] indent_style = tab -[*.{received,verified}.txt] -insert_final_newline = false -trim_trailing_whitespace = false - [*.{cs,csx,vb,vbx}] # .NET Code Style Settings # See https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference @@ -132,9 +128,16 @@ dotnet_naming_style.all_const.capitalization = pascal dotnet_naming_symbols.all_const.applicable_kinds = field dotnet_naming_symbols.all_const.required_modifiers = const dotnet_naming_rule.all_const.severity = error -dotnet_naming_rule.all_const.style = all_elements +dotnet_naming_rule.all_const.style = all_const dotnet_naming_rule.all_const.symbols = all_const +dotnet_naming_style.all_static_readonly.capitalization = pascal_case +dotnet_naming_symbols.all_static_readonly.applicable_kinds = field +dotnet_naming_symbols.all_static_readonly.required_modifiers = static, readonly +dotnet_naming_rule.all_static_readonly.severity = error +dotnet_naming_rule.all_static_readonly.style = all_static_readonly +dotnet_naming_rule.all_static_readonly.symbols = all_static_readonly + dotnet_naming_style.all_fields.required_prefix = _ dotnet_naming_style.all_fields.capitalization = camel_case dotnet_naming_symbols.all_fields.applicable_kinds = field @@ -267,18 +270,6 @@ dotnet_diagnostic.IDE0290.severity = sugges # [CSharpier] Incompatible rules deactivated # https://csharpier.com/docs/IntegratingWithLinters#code-analysis-rules dotnet_diagnostic.IDE0055.severity = none -dotnet_diagnostic.SA1000.severity = none -dotnet_diagnostic.SA1009.severity = none -dotnet_diagnostic.SA1111.severity = none -dotnet_diagnostic.SA1118.severity = none -dotnet_diagnostic.SA1137.severity = none -dotnet_diagnostic.SA1413.severity = none -dotnet_diagnostic.SA1500.severity = none -dotnet_diagnostic.SA1501.severity = none -dotnet_diagnostic.SA1502.severity = none -dotnet_diagnostic.SA1504.severity = none -dotnet_diagnostic.SA1515.severity = none -dotnet_diagnostic.SA1516.severity = none # Support for NetEvolve.Arguments Methods # https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1062#null-check-validation-methods diff --git a/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.Append.cs b/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.Append.cs index 2f198a9..8f3cae5 100644 --- a/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.Append.cs +++ b/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.Append.cs @@ -1,15 +1,15 @@ -namespace NetEvolve.CodeBuilder; +namespace NetEvolve.CodeBuilder; using System; -public partial record CSharpCodeBuilder +public partial class CSharpCodeBuilder { /// /// Appends a boolean value to the current builder. /// /// The boolean value to append. /// The current instance to allow for method chaining. - /// Appends either "True" or "False" based on the value. + /// Appends either or based on the value. public CSharpCodeBuilder Append(bool value) { EnsureIndented(); @@ -36,6 +36,16 @@ public CSharpCodeBuilder Append(char value, int repeatCount) /// /// The character to append. /// The current instance to allow for method chaining. + /// + /// The following characters receive special treatment: + /// + /// '\0' — ignored; the method returns without appending. + /// '\n' or '\r' — treated as a line terminator; equivalent to calling . + /// '{' or '[' — appended with the current indentation, then the indentation level is incremented and a line terminator is added. + /// '}' or ']' — the indentation level is decremented first, then the character is appended with the new indentation level, followed by a line terminator. + /// + /// All other characters are appended after applying the current indentation at the start of a new line. + /// public CSharpCodeBuilder Append(char value) { if (value is '\0') @@ -164,6 +174,52 @@ public CSharpCodeBuilder Append(ReadOnlyMemory value, int startIndex, int return this; } + /// + /// Appends a read-only span of characters to the current builder. + /// + /// The read-only span containing the characters to append. + /// The current instance to allow for method chaining. + /// If the span is empty, the method returns without appending anything. + public CSharpCodeBuilder Append(ReadOnlySpan value) + { + if (value.IsEmpty) + { + return this; + } + + EnsureIndented(); +#if NETSTANDARD2_0 + _ = _builder.Append(value.ToString()); +#else + _ = _builder.Append(value); +#endif + return this; + } + + /// + /// Appends a subset of a read-only span of characters to the current builder. + /// + /// The read-only span containing the characters to append. + /// The starting position in the span. + /// The number of characters to append. + /// The current instance to allow for method chaining. + /// If the span is empty, the method returns without appending anything. + public CSharpCodeBuilder Append(ReadOnlySpan value, int startIndex, int count) + { + if (value.IsEmpty) + { + return this; + } + + EnsureIndented(); +#if NETSTANDARD2_0 + _ = _builder.Append(value.Slice(startIndex, count).ToString()); +#else + _ = _builder.Append(value.Slice(startIndex, count)); +#endif + return this; + } + /// /// Appends a subset of a string to the current builder. /// @@ -189,7 +245,16 @@ public CSharpCodeBuilder Append(string? value, int startIndex, int count) /// /// The string to append. /// The current instance to allow for method chaining. - /// If the string is null or empty, the method returns without appending anything. + /// + /// null, an empty string, or "\0" are ignored; the method returns without appending. + /// The following single-character strings receive special treatment: + /// + /// "\n", "\r", or "\r\n" — treated as a line terminator; equivalent to calling . + /// "{" or "[" — appended with the current indentation, then the indentation level is incremented and a line terminator is added. + /// "}" or "]" — the indentation level is decremented first; if the current position is mid-line, a line terminator is inserted before the character. The character is then appended with the new indentation level, followed by a line terminator. This behavior is consistent with . + /// + /// All other strings are appended after applying the current indentation at the start of a new line. + /// public CSharpCodeBuilder Append(string? value) { if (string.IsNullOrEmpty(value) || value is "\0") @@ -205,7 +270,10 @@ public CSharpCodeBuilder Append(string? value) if (value is "}" or "]") { DecrementIndent(); - _ = AppendLine(); + if (!_isNewline) + { + _ = AppendLine(); // Ensure we start a new line before the closing brace + } } EnsureIndented(); @@ -216,6 +284,10 @@ public CSharpCodeBuilder Append(string? value) IncrementIndent(); _ = AppendLine(); } + else if (value is "}" or "]") + { + _ = AppendLine(); // Newline after closing brace, consistent with char overload + } return this; } diff --git a/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.AppendFormat.cs b/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.AppendFormat.cs index 5cb118d..8b7b608 100644 --- a/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.AppendFormat.cs +++ b/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.AppendFormat.cs @@ -1,10 +1,10 @@ -namespace NetEvolve.CodeBuilder; +namespace NetEvolve.CodeBuilder; using System; using System.Globalization; using System.Runtime.CompilerServices; -public partial record CSharpCodeBuilder +public partial class CSharpCodeBuilder { /// /// Appends a formatted string to the current builder using invariant culture. diff --git a/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.AppendIf.cs b/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.AppendIf.cs index 1feb663..fd1f07d 100644 --- a/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.AppendIf.cs +++ b/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.AppendIf.cs @@ -1,9 +1,9 @@ -namespace NetEvolve.CodeBuilder; +namespace NetEvolve.CodeBuilder; using System; using System.Runtime.CompilerServices; -public partial record CSharpCodeBuilder +public partial class CSharpCodeBuilder { /// /// Appends a boolean value to the current builder if the specified condition is true. @@ -11,7 +11,7 @@ public partial record CSharpCodeBuilder /// The condition that determines whether to append the value. /// The boolean value to append. /// The current instance to allow for method chaining. - /// Appends either "true" or "false" based on the value if the condition is true. + /// Appends either or based on the value if the condition is . [MethodImpl(MethodImplOptions.AggressiveInlining)] public CSharpCodeBuilder AppendIf(bool condition, bool value) => condition ? Append(value) : this; @@ -94,6 +94,29 @@ public unsafe CSharpCodeBuilder AppendIf(bool condition, char* value, int length public CSharpCodeBuilder AppendIf(bool condition, ReadOnlyMemory value, int startIndex, int count) => condition ? Append(value, startIndex, count) : this; + /// + /// Appends a read-only span of characters to the current builder if the specified condition is . + /// + /// The condition that determines whether to append the value. + /// The read-only span containing the characters to append. + /// The current instance to allow for method chaining. + /// If the span is empty, the method returns without appending anything. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public CSharpCodeBuilder AppendIf(bool condition, ReadOnlySpan value) => condition ? Append(value) : this; + + /// + /// Appends a subset of a read-only span of characters to the current builder if the specified condition is . + /// + /// The condition that determines whether to append the value. + /// The read-only span containing the characters to append. + /// The starting position in the span. + /// The number of characters to append. + /// The current instance to allow for method chaining. + /// If the span is empty, the method returns without appending anything. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public CSharpCodeBuilder AppendIf(bool condition, ReadOnlySpan value, int startIndex, int count) => + condition ? Append(value, startIndex, count) : this; + /// /// Appends a subset of a string to the current builder if the specified condition is true. /// diff --git a/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.AppendLine.cs b/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.AppendLine.cs index b612b70..3079376 100644 --- a/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.AppendLine.cs +++ b/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.AppendLine.cs @@ -1,9 +1,9 @@ -namespace NetEvolve.CodeBuilder; +namespace NetEvolve.CodeBuilder; using System; using System.Runtime.CompilerServices; -public partial record CSharpCodeBuilder +public partial class CSharpCodeBuilder { /// /// Appends a line terminator to the current builder. @@ -51,6 +51,27 @@ public CSharpCodeBuilder AppendLine() public CSharpCodeBuilder AppendLine(ReadOnlyMemory value, int startIndex, int count) => Append(value, startIndex, count).AppendLine(); + /// + /// Appends a read-only span of characters followed by a line terminator to the current builder. + /// + /// The read-only span containing the characters to append. + /// The current instance to allow for method chaining. + /// If the span is empty, only the line terminator is appended. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public CSharpCodeBuilder AppendLine(ReadOnlySpan value) => Append(value).AppendLine(); + + /// + /// Appends a subset of a read-only span of characters followed by a line terminator to the current builder. + /// + /// The read-only span containing the characters to append. + /// The starting position in the span. + /// The number of characters to append. + /// The current instance to allow for method chaining. + /// If the span is empty, only the line terminator is appended. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public CSharpCodeBuilder AppendLine(ReadOnlySpan value, int startIndex, int count) => + Append(value, startIndex, count).AppendLine(); + /// /// Appends an array of characters followed by a line terminator to the current builder. /// @@ -105,7 +126,7 @@ public CSharpCodeBuilder AppendLine(char[]? value, int startIndex, int charCount /// /// The boolean value to append. /// The current instance to allow for method chaining. - /// Appends either "true" or "false" based on the value, followed by a line terminator. + /// Appends either or based on the value, followed by a line terminator. [MethodImpl(MethodImplOptions.AggressiveInlining)] public CSharpCodeBuilder AppendLine(bool value) => Append(value).AppendLine(); } diff --git a/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.AppendLineIf.cs b/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.AppendLineIf.cs index 18e6c6b..bf8827f 100644 --- a/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.AppendLineIf.cs +++ b/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.AppendLineIf.cs @@ -1,9 +1,9 @@ -namespace NetEvolve.CodeBuilder; +namespace NetEvolve.CodeBuilder; using System; using System.Runtime.CompilerServices; -public partial record CSharpCodeBuilder +public partial class CSharpCodeBuilder { /// /// Appends a line terminator to the current builder if the specified condition is true. @@ -47,6 +47,30 @@ public CSharpCodeBuilder AppendLineIf(bool condition, ReadOnlyMemory value public CSharpCodeBuilder AppendLineIf(bool condition, ReadOnlyMemory value, int startIndex, int count) => condition ? AppendLine(value, startIndex, count) : this; + /// + /// Appends a read-only span of characters followed by a line terminator to the current builder if the specified condition is . + /// + /// The condition that determines whether to append the value. + /// The read-only span containing the characters to append. + /// The current instance to allow for method chaining. + /// If the span is empty, only the line terminator is appended. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public CSharpCodeBuilder AppendLineIf(bool condition, ReadOnlySpan value) => + condition ? AppendLine(value) : this; + + /// + /// Appends a subset of a read-only span of characters followed by a line terminator to the current builder if the specified condition is . + /// + /// The condition that determines whether to append the value. + /// The read-only span containing the characters to append. + /// The starting position in the span. + /// The number of characters to append. + /// The current instance to allow for method chaining. + /// If the span is empty, only the line terminator is appended. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public CSharpCodeBuilder AppendLineIf(bool condition, ReadOnlySpan value, int startIndex, int count) => + condition ? AppendLine(value, startIndex, count) : this; + /// /// Appends an array of characters followed by a line terminator to the current builder if the specified condition is true. /// @@ -109,7 +133,7 @@ public CSharpCodeBuilder AppendLineIf(bool condition, char value, int repeatCoun /// The condition that determines whether to append the value. /// The boolean value to append. /// The current instance to allow for method chaining. - /// Appends either "true" or "false" based on the value if the condition is true. + /// Appends either or based on the value if the condition is . [MethodImpl(MethodImplOptions.AggressiveInlining)] public CSharpCodeBuilder AppendLineIf(bool condition, bool value) => condition ? AppendLine(value) : this; } diff --git a/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.Clear.cs b/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.Clear.cs index 036afea..56bbe56 100644 --- a/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.Clear.cs +++ b/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.Clear.cs @@ -1,19 +1,21 @@ -namespace NetEvolve.CodeBuilder; +namespace NetEvolve.CodeBuilder; -public partial record CSharpCodeBuilder +public partial class CSharpCodeBuilder { /// /// Clears the content of the current builder and resets the indentation level to zero. /// /// The current instance to allow for method chaining. /// - /// This method removes all characters from the internal and - /// resets the indentation level to zero, effectively providing a clean slate for building new content. + /// This method removes all characters from the internal , + /// resets the indentation level to zero, and resets the internal newline-tracking state, + /// effectively providing a clean slate for building new content. /// public CSharpCodeBuilder Clear() { _ = _builder.Clear(); - _ = Interlocked.Exchange(ref _indentLevel, 0); + _indentLevel = 0; + _isNewline = true; return this; } @@ -29,10 +31,16 @@ public CSharpCodeBuilder Clear() /// Unlike automatic indentation that occurs at the start of new lines, this method provides manual control /// over indentation placement and does not modify the current indentation level. /// - public CSharpCodeBuilder Intend() + public CSharpCodeBuilder Indent() { _ = _builder.Append(UseTabs ? '\t' : ' ', UseTabs ? 1 : 4); return this; } + + /// +#pragma warning disable S1133 // Deprecated code should be removed + [Obsolete("Use Indent() instead. This method will be removed in a future version.")] +#pragma warning restore S1133 // Deprecated code should be removed + public CSharpCodeBuilder Intend() => Indent(); } diff --git a/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.Documentation.cs b/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.Documentation.cs index c889591..2119756 100644 --- a/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.Documentation.cs +++ b/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.Documentation.cs @@ -1,9 +1,9 @@ -namespace NetEvolve.CodeBuilder; +namespace NetEvolve.CodeBuilder; using System.Collections.Generic; using System.Runtime.CompilerServices; -public partial record CSharpCodeBuilder +public partial class CSharpCodeBuilder { /// /// Appends a single-line XML documentation comment. @@ -13,7 +13,7 @@ public partial record CSharpCodeBuilder /// If the content is null or empty, the method returns without appending anything. [MethodImpl(MethodImplOptions.AggressiveInlining)] public CSharpCodeBuilder AppendXmlDoc(string? content) => - string.IsNullOrEmpty(content) ? this : EnsureNewLineForXmlDoc().AppendLine($"/// {content}"); + string.IsNullOrEmpty(content) ? this : EnsureNewLineForXmlDoc().Append("/// ").AppendLine(content); /// /// Appends an XML documentation summary element. @@ -30,7 +30,8 @@ public CSharpCodeBuilder AppendXmlDocSummary(string? summary) return EnsureNewLineForXmlDoc() .AppendLine("/// ") - .AppendLine($"/// {summary}") + .Append("/// ") + .AppendLine(summary) .AppendLine("/// "); } @@ -39,7 +40,7 @@ public CSharpCodeBuilder AppendXmlDocSummary(string? summary) /// /// The summary lines to include in the documentation. /// The current instance to allow for method chaining. - /// If the summary lines collection is null or empty, the method returns without appending anything. + /// If is , empty, or every element is or empty, the method returns without appending anything. public CSharpCodeBuilder AppendXmlDocSummary(IEnumerable? summaryLines) { if (summaryLines is null) @@ -47,16 +48,15 @@ public CSharpCodeBuilder AppendXmlDocSummary(IEnumerable? summaryLines) return this; } - var hasContent = false; - var builder = EnsureNewLineForXmlDoc().AppendLine("/// "); + CSharpCodeBuilder? builder = null; foreach (var line in summaryLines.Where(l => !string.IsNullOrEmpty(l))) { - builder = builder.AppendLine($"/// {line}"); - hasContent = true; + builder ??= EnsureNewLineForXmlDoc().AppendLine("/// "); + builder = builder.Append("/// ").AppendLine(line); } - return hasContent ? builder.AppendLine("/// ") : this; + return builder?.AppendLine("/// ") ?? this; } /// @@ -73,7 +73,12 @@ public CSharpCodeBuilder AppendXmlDocParam(string? paramName, string? descriptio return this; } - return EnsureNewLineForXmlDoc().AppendLine($"/// {description}"); + return EnsureNewLineForXmlDoc() + .Append("/// ") + .Append(description) + .AppendLine(""); } /// @@ -111,7 +116,7 @@ public CSharpCodeBuilder AppendXmlDocReturns(string? description) return this; } - return EnsureNewLineForXmlDoc().AppendLine($"/// {description}"); + return EnsureNewLineForXmlDoc().Append("/// ").Append(description).AppendLine(""); } /// @@ -139,7 +144,7 @@ public CSharpCodeBuilder AppendXmlDocRemarks(string? remarks) /// /// The remarks lines to include in the documentation. /// The current instance to allow for method chaining. - /// If the remarks lines collection is null or empty, the method returns without appending anything. + /// If is , empty, or every element is or empty, the method returns without appending anything. public CSharpCodeBuilder AppendXmlDocRemarks(IEnumerable? remarksLines) { if (remarksLines is null) @@ -147,16 +152,15 @@ public CSharpCodeBuilder AppendXmlDocRemarks(IEnumerable? remarksLines) return this; } - var hasContent = false; - var builder = EnsureNewLineForXmlDoc().AppendLine("/// "); + CSharpCodeBuilder? builder = null; foreach (var line in remarksLines.Where(l => !string.IsNullOrEmpty(l))) { - builder = builder.AppendLine($"/// {line}"); - hasContent = true; + builder ??= EnsureNewLineForXmlDoc().AppendLine("/// "); + builder = builder.Append("/// ").AppendLine(line); } - return hasContent ? builder.AppendLine("/// ") : this; + return builder?.AppendLine("/// ") ?? this; } /// @@ -174,7 +178,11 @@ public CSharpCodeBuilder AppendXmlDocException(string? exceptionType, string? de } return EnsureNewLineForXmlDoc() - .AppendLine($"/// {description}"); + .Append("/// ") + .Append(description) + .AppendLine(""); } /// @@ -225,7 +233,8 @@ public CSharpCodeBuilder AppendXmlDocExample(string? example) return EnsureNewLineForXmlDoc() .AppendLine("/// ") - .AppendLine($"/// {example}") + .Append("/// ") + .AppendLine(example) .AppendLine("/// "); } @@ -234,7 +243,7 @@ public CSharpCodeBuilder AppendXmlDocExample(string? example) /// /// The example lines to include in the documentation. /// The current instance to allow for method chaining. - /// If the example lines collection is null or empty, the method returns without appending anything. + /// If is , empty, or every element is or empty, the method returns without appending anything. public CSharpCodeBuilder AppendXmlDocExample(IEnumerable? exampleLines) { if (exampleLines is null) @@ -242,23 +251,22 @@ public CSharpCodeBuilder AppendXmlDocExample(IEnumerable? exampleLines) return this; } - var hasContent = false; - var builder = EnsureNewLineForXmlDoc().AppendLine("/// "); + CSharpCodeBuilder? builder = null; foreach (var line in exampleLines.Where(l => !string.IsNullOrEmpty(l))) { - builder = builder.AppendLine($"/// {line}"); - hasContent = true; + builder ??= EnsureNewLineForXmlDoc().AppendLine("/// "); + builder = builder.Append("/// ").AppendLine(line); } - return hasContent ? builder.AppendLine("/// ") : this; + return builder?.AppendLine("/// ") ?? this; } /// /// Appends an XML documentation see element for cross-references. /// /// The cross-reference to another member or type. - /// If set to true, uses 'href' instead of 'cref' for external links. + /// If set to , uses 'href' instead of 'cref' for external links. /// The current instance to allow for method chaining. /// If the cref is null or empty, the method returns without appending anything. public CSharpCodeBuilder AppendXmlDocSee(string? cref, bool isHref = false) @@ -268,14 +276,19 @@ public CSharpCodeBuilder AppendXmlDocSee(string? cref, bool isHref = false) return this; } - return EnsureNewLineForXmlDoc().AppendLine($"/// "); + return EnsureNewLineForXmlDoc() + .Append("/// "); } /// /// Appends an XML documentation seealso element for see-also references. /// /// The cross-reference to another member or type. - /// If set to true, uses 'href' instead of 'cref' for external links. + /// If set to , uses 'href' instead of 'cref' for external links. /// The current instance to allow for method chaining. /// If the cref is null or empty, the method returns without appending anything. public CSharpCodeBuilder AppendXmlDocSeeAlso(string? cref, bool isHref = false) @@ -285,7 +298,12 @@ public CSharpCodeBuilder AppendXmlDocSeeAlso(string? cref, bool isHref = false) return this; } - return EnsureNewLineForXmlDoc().AppendLine($"/// "); + return EnsureNewLineForXmlDoc() + .Append("/// "); } /// @@ -301,7 +319,7 @@ public CSharpCodeBuilder AppendXmlDocValue(string? description) return this; } - return EnsureNewLineForXmlDoc().AppendLine($"/// {description}"); + return EnsureNewLineForXmlDoc().Append("/// ").Append(description).AppendLine(""); } /// @@ -318,7 +336,12 @@ public CSharpCodeBuilder AppendXmlDocTypeParam(string? paramName, string? descri return this; } - return EnsureNewLineForXmlDoc().AppendLine($"/// {description}"); + return EnsureNewLineForXmlDoc() + .Append("/// ") + .Append(description) + .AppendLine(""); } /// @@ -352,7 +375,7 @@ public CSharpCodeBuilder AppendXmlDocTypeParams(IEnumerable<(string, string)>? t public CSharpCodeBuilder AppendXmlDocInheritDoc(string? cref = null) => string.IsNullOrEmpty(cref) ? EnsureNewLineForXmlDoc().AppendLine("/// ") - : EnsureNewLineForXmlDoc().AppendLine($"/// "); + : EnsureNewLineForXmlDoc().Append("/// "); /// /// Appends a custom XML documentation element. @@ -373,14 +396,19 @@ public CSharpCodeBuilder AppendXmlDocCustomElement( return this; } - var attributesPart = string.IsNullOrEmpty(attributes) ? string.Empty : $" {attributes}"; + var docBuilder = EnsureNewLineForXmlDoc().Append("/// <").Append(elementName); + + if (!string.IsNullOrEmpty(attributes)) + { + _ = docBuilder.Append(' ').Append(attributes); + } if (string.IsNullOrEmpty(content)) { - return EnsureNewLineForXmlDoc().AppendLine($"/// <{elementName}{attributesPart} />"); + return docBuilder.AppendLine(" />"); } - return EnsureNewLineForXmlDoc().AppendLine($"/// <{elementName}{attributesPart}>{content}"); + return docBuilder.Append(">").Append(content).Append(""); } /// diff --git a/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.EnsureCapacity.cs b/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.EnsureCapacity.cs index 39c4423..4536198 100644 --- a/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.EnsureCapacity.cs +++ b/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.EnsureCapacity.cs @@ -1,6 +1,6 @@ -namespace NetEvolve.CodeBuilder; +namespace NetEvolve.CodeBuilder; -public partial record CSharpCodeBuilder +public partial class CSharpCodeBuilder { /// /// Ensures that the capacity of the internal is at least the specified value. diff --git a/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.Scope.cs b/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.Scope.cs index 7a74544..ae8a233 100644 --- a/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.Scope.cs +++ b/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.Scope.cs @@ -2,7 +2,7 @@ namespace NetEvolve.CodeBuilder; using System; -public partial record CSharpCodeBuilder +public partial class CSharpCodeBuilder { /// /// Creates a scope that automatically manages indentation levels. @@ -82,14 +82,14 @@ internal ScopeHandler(CSharpCodeBuilder builder) /// Determines whether the specified object is equal to the current instance. /// /// The object to compare with the current instance. - /// Always returns false since ScopeHandler instances should not be compared. + /// Always returns since ScopeHandler instances should not be compared. public override readonly bool Equals(object? obj) => false; /// /// Determines whether the specified ScopeHandler is equal to the current instance. /// /// The ScopeHandler to compare with the current instance. - /// Always returns false since ScopeHandler instances should not be compared. + /// Always returns since ScopeHandler instances should not be compared. public readonly bool Equals(ScopeHandler other) => false; /// @@ -103,7 +103,7 @@ internal ScopeHandler(CSharpCodeBuilder builder) /// /// The first instance to compare. /// The second instance to compare. - /// Always returns false since ScopeHandler instances should not be compared. + /// Always returns since ScopeHandler instances should not be compared. public static bool operator ==(ScopeHandler _, ScopeHandler __) => false; /// @@ -111,7 +111,7 @@ internal ScopeHandler(CSharpCodeBuilder builder) /// /// The first instance to compare. /// The second instance to compare. - /// Always returns true since ScopeHandler instances should not be compared. + /// Always returns since ScopeHandler instances should not be compared. public static bool operator !=(ScopeHandler _, ScopeHandler __) => true; } } diff --git a/src/NetEvolve.CodeBuilder/CSharpCodeBuilder._EMPTY_.cs b/src/NetEvolve.CodeBuilder/CSharpCodeBuilder._EMPTY_.cs index 7682b2f..da64ca1 100644 --- a/src/NetEvolve.CodeBuilder/CSharpCodeBuilder._EMPTY_.cs +++ b/src/NetEvolve.CodeBuilder/CSharpCodeBuilder._EMPTY_.cs @@ -1,3 +1,3 @@ -namespace NetEvolve.CodeBuilder; +namespace NetEvolve.CodeBuilder; -public partial record CSharpCodeBuilder { } +public partial class CSharpCodeBuilder { } diff --git a/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.cs b/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.cs index bd4d747..af58825 100644 --- a/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.cs +++ b/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.cs @@ -1,11 +1,11 @@ -namespace NetEvolve.CodeBuilder; +namespace NetEvolve.CodeBuilder; using System.Text; /// /// Provides functionality for building C# code strings with proper indentation. /// -public partial record CSharpCodeBuilder : CodeBuilderBase +public partial class CSharpCodeBuilder : CodeBuilderBase { /// /// Initializes a new instance of the class. diff --git a/src/NetEvolve.CodeBuilder/CodeBuilderBase.EnsureIndented.cs b/src/NetEvolve.CodeBuilder/CodeBuilderBase.EnsureIndented.cs index 9607d11..9374547 100644 --- a/src/NetEvolve.CodeBuilder/CodeBuilderBase.EnsureIndented.cs +++ b/src/NetEvolve.CodeBuilder/CodeBuilderBase.EnsureIndented.cs @@ -1,8 +1,8 @@ -namespace NetEvolve.CodeBuilder; +namespace NetEvolve.CodeBuilder; using System.Runtime.CompilerServices; -public partial record CodeBuilderBase +public partial class CodeBuilderBase { /// /// Ensures that indentation is applied if we are at the start of a new line. @@ -36,24 +36,24 @@ private protected void EnsureIndented(bool deactivate = false) /// /// /// This method increases the current indentation level, which affects subsequent lines - /// that are appended to the builder. The operation is thread-safe. + /// that are appended to the builder. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void IncrementIndent() => Interlocked.Increment(ref _indentLevel); + internal void IncrementIndent() => _indentLevel++; /// /// Decrements the indentation level by one. /// /// /// This method decreases the current indentation level, which affects subsequent lines - /// that are appended to the builder. The operation is thread-safe. + /// that are appended to the builder. The indentation level will not go below zero. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void DecrementIndent() { - if (Interlocked.Decrement(ref _indentLevel) < 0) + if (--_indentLevel < 0) { - _ = Interlocked.Exchange(ref _indentLevel, 0); + _indentLevel = 0; } } } diff --git a/src/NetEvolve.CodeBuilder/CodeBuilderBase.ToString.cs b/src/NetEvolve.CodeBuilder/CodeBuilderBase.ToString.cs index 60cffdb..365d9a2 100644 --- a/src/NetEvolve.CodeBuilder/CodeBuilderBase.ToString.cs +++ b/src/NetEvolve.CodeBuilder/CodeBuilderBase.ToString.cs @@ -1,6 +1,6 @@ -namespace NetEvolve.CodeBuilder; +namespace NetEvolve.CodeBuilder; -public partial record CodeBuilderBase +public partial class CodeBuilderBase { /// /// Returns the string that has been built by this . diff --git a/src/NetEvolve.CodeBuilder/CodeBuilderBase.cs b/src/NetEvolve.CodeBuilder/CodeBuilderBase.cs index 6397834..054809b 100644 --- a/src/NetEvolve.CodeBuilder/CodeBuilderBase.cs +++ b/src/NetEvolve.CodeBuilder/CodeBuilderBase.cs @@ -1,11 +1,15 @@ -namespace NetEvolve.CodeBuilder; +namespace NetEvolve.CodeBuilder; using System.Text; /// /// Provides the base functionality for building code strings with proper indentation and formatting. /// -public abstract partial record CodeBuilderBase +/// +/// This class and its derived types are not thread-safe. All operations on a single instance +/// must be performed from a single thread. +/// +public abstract partial class CodeBuilderBase { private protected readonly StringBuilder _builder; private protected int _indentLevel; @@ -37,6 +41,6 @@ public abstract partial record CodeBuilderBase /// /// Gets or sets a value indicating whether to use tabs instead of spaces for indentation. /// - /// true to use tabs for indentation; false to use spaces. + /// to use tabs for indentation; to use spaces. public bool UseTabs { get; set; } } diff --git a/tests/NetEvolve.CodeBuilder.Tests.Integration/_snapshots/CSharpCodeBuilderTests.GenerateClassWithFormatting_UsingSpaces_Should_ProduceCorrectIndentation.verified.txt b/tests/NetEvolve.CodeBuilder.Tests.Integration/_snapshots/CSharpCodeBuilderTests.GenerateClassWithFormatting_UsingSpaces_Should_ProduceCorrectIndentation.verified.txt index 4a16bd3..adb3ad3 100644 --- a/tests/NetEvolve.CodeBuilder.Tests.Integration/_snapshots/CSharpCodeBuilderTests.GenerateClassWithFormatting_UsingSpaces_Should_ProduceCorrectIndentation.verified.txt +++ b/tests/NetEvolve.CodeBuilder.Tests.Integration/_snapshots/CSharpCodeBuilderTests.GenerateClassWithFormatting_UsingSpaces_Should_ProduceCorrectIndentation.verified.txt @@ -8,12 +8,14 @@ Console.WriteLine("Nested"); } } + public void Method2() { try{ DoSomething(); - }catch (Exception ex){ + } + catch (Exception ex){ Console.WriteLine($"Error: {ex.Message}"); } } -} \ No newline at end of file +} diff --git a/tests/NetEvolve.CodeBuilder.Tests.Integration/_snapshots/CSharpCodeBuilderTests.GenerateClassWithFormatting_UsingTabs_Should_ProduceCorrectIndentation.verified.txt b/tests/NetEvolve.CodeBuilder.Tests.Integration/_snapshots/CSharpCodeBuilderTests.GenerateClassWithFormatting_UsingTabs_Should_ProduceCorrectIndentation.verified.txt index 94d0ddc..d315382 100644 --- a/tests/NetEvolve.CodeBuilder.Tests.Integration/_snapshots/CSharpCodeBuilderTests.GenerateClassWithFormatting_UsingTabs_Should_ProduceCorrectIndentation.verified.txt +++ b/tests/NetEvolve.CodeBuilder.Tests.Integration/_snapshots/CSharpCodeBuilderTests.GenerateClassWithFormatting_UsingTabs_Should_ProduceCorrectIndentation.verified.txt @@ -8,12 +8,14 @@ Console.WriteLine("Nested"); } } + public void Method2() { try{ DoSomething(); - }catch (Exception ex){ + } + catch (Exception ex){ Console.WriteLine($"Error: {ex.Message}"); } } -} \ No newline at end of file +} diff --git a/tests/NetEvolve.CodeBuilder.Tests.Integration/_snapshots/CSharpCodeBuilderTests.GenerateCompleteClass_Should_MatchSnapshot.verified.txt b/tests/NetEvolve.CodeBuilder.Tests.Integration/_snapshots/CSharpCodeBuilderTests.GenerateCompleteClass_Should_MatchSnapshot.verified.txt index d164a1e..e890098 100644 --- a/tests/NetEvolve.CodeBuilder.Tests.Integration/_snapshots/CSharpCodeBuilderTests.GenerateCompleteClass_Should_MatchSnapshot.verified.txt +++ b/tests/NetEvolve.CodeBuilder.Tests.Integration/_snapshots/CSharpCodeBuilderTests.GenerateCompleteClass_Should_MatchSnapshot.verified.txt @@ -8,4 +8,4 @@ namespace MyApplication.Models public string Id { get; set; } public string Name { get; set; } } -} \ No newline at end of file +} diff --git a/tests/NetEvolve.CodeBuilder.Tests.Integration/_snapshots/CSharpCodeBuilderTests.GenerateCompleteClass_Should_ProduceCorrectOutput.verified.txt b/tests/NetEvolve.CodeBuilder.Tests.Integration/_snapshots/CSharpCodeBuilderTests.GenerateCompleteClass_Should_ProduceCorrectOutput.verified.txt index 4a96ddd..13e65e4 100644 --- a/tests/NetEvolve.CodeBuilder.Tests.Integration/_snapshots/CSharpCodeBuilderTests.GenerateCompleteClass_Should_ProduceCorrectOutput.verified.txt +++ b/tests/NetEvolve.CodeBuilder.Tests.Integration/_snapshots/CSharpCodeBuilderTests.GenerateCompleteClass_Should_ProduceCorrectOutput.verified.txt @@ -17,8 +17,8 @@ namespace MyApplication.Models public Customer(string id) { _id = id ?? throw new ArgumentNullException(nameof(id)); - } + /// /// Gets the customer identifier. /// @@ -34,4 +34,4 @@ namespace MyApplication.Models /// public string? Email { get; set; } } -} \ No newline at end of file +} diff --git a/tests/NetEvolve.CodeBuilder.Tests.Integration/_snapshots/CSharpCodeBuilderTests.GenerateEnum_WithDocumentation_Should_ProduceCorrectOutput.verified.txt b/tests/NetEvolve.CodeBuilder.Tests.Integration/_snapshots/CSharpCodeBuilderTests.GenerateEnum_WithDocumentation_Should_ProduceCorrectOutput.verified.txt index 3a00a9e..3d2a5d7 100644 --- a/tests/NetEvolve.CodeBuilder.Tests.Integration/_snapshots/CSharpCodeBuilderTests.GenerateEnum_WithDocumentation_Should_ProduceCorrectOutput.verified.txt +++ b/tests/NetEvolve.CodeBuilder.Tests.Integration/_snapshots/CSharpCodeBuilderTests.GenerateEnum_WithDocumentation_Should_ProduceCorrectOutput.verified.txt @@ -28,4 +28,4 @@ namespace MyApplication.Enums /// Delivered = 8 } -} \ No newline at end of file +} diff --git a/tests/NetEvolve.CodeBuilder.Tests.Integration/_snapshots/CSharpCodeBuilderTests.GenerateInterface_WithMultipleMethods_Should_ProduceCorrectOutput.verified.txt b/tests/NetEvolve.CodeBuilder.Tests.Integration/_snapshots/CSharpCodeBuilderTests.GenerateInterface_WithMultipleMethods_Should_ProduceCorrectOutput.verified.txt index b927e3c..32821b7 100644 --- a/tests/NetEvolve.CodeBuilder.Tests.Integration/_snapshots/CSharpCodeBuilderTests.GenerateInterface_WithMultipleMethods_Should_ProduceCorrectOutput.verified.txt +++ b/tests/NetEvolve.CodeBuilder.Tests.Integration/_snapshots/CSharpCodeBuilderTests.GenerateInterface_WithMultipleMethods_Should_ProduceCorrectOutput.verified.txt @@ -29,4 +29,4 @@ namespace MyApplication.Services /// A task representing the asynchronous operation. Task UpdateCustomerAsync(Customer customer); } -} \ No newline at end of file +} diff --git a/tests/NetEvolve.CodeBuilder.Tests.Integration/_snapshots/CSharpCodeBuilderTests.GenerateMethodWithConditionalContent_Should_ProduceCorrectOutput.verified.txt b/tests/NetEvolve.CodeBuilder.Tests.Integration/_snapshots/CSharpCodeBuilderTests.GenerateMethodWithConditionalContent_Should_ProduceCorrectOutput.verified.txt index 9e7e44b..3d28618 100644 --- a/tests/NetEvolve.CodeBuilder.Tests.Integration/_snapshots/CSharpCodeBuilderTests.GenerateMethodWithConditionalContent_Should_ProduceCorrectOutput.verified.txt +++ b/tests/NetEvolve.CodeBuilder.Tests.Integration/_snapshots/CSharpCodeBuilderTests.GenerateMethodWithConditionalContent_Should_ProduceCorrectOutput.verified.txt @@ -6,6 +6,5 @@ Console.WriteLine($"Processing complete: {result}"); await Task.CompletedTask; return result; - } -} \ No newline at end of file +} diff --git a/tests/NetEvolve.CodeBuilder.Tests.Integration/_snapshots/CSharpCodeBuilderTests.GenerateReflectionBasedCode_Should_ProduceCorrectOutput.verified.txt b/tests/NetEvolve.CodeBuilder.Tests.Integration/_snapshots/CSharpCodeBuilderTests.GenerateReflectionBasedCode_Should_ProduceCorrectOutput.verified.txt index f4150dc..32ae849 100644 --- a/tests/NetEvolve.CodeBuilder.Tests.Integration/_snapshots/CSharpCodeBuilderTests.GenerateReflectionBasedCode_Should_ProduceCorrectOutput.verified.txt +++ b/tests/NetEvolve.CodeBuilder.Tests.Integration/_snapshots/CSharpCodeBuilderTests.GenerateReflectionBasedCode_Should_ProduceCorrectOutput.verified.txt @@ -9,8 +9,8 @@ public class GeneratedEntity { _ID = ID; _CREATEDAT = CREATEDAT; - } + public int Id => _ID; public string? Name { get; set; } @@ -19,5 +19,4 @@ public class GeneratedEntity public DateTime CreatedAt => _CREATEDAT; - -} \ No newline at end of file +} diff --git a/tests/NetEvolve.CodeBuilder.Tests.Integration/_snapshots/CSharpCodeBuilderTests.GenerateWithDifferentFormats_Should_MatchSnapshots_02b3255120757173.verified.txt b/tests/NetEvolve.CodeBuilder.Tests.Integration/_snapshots/CSharpCodeBuilderTests.GenerateWithDifferentFormats_Should_MatchSnapshots_02b3255120757173.verified.txt index 271daf7..83599ec 100644 --- a/tests/NetEvolve.CodeBuilder.Tests.Integration/_snapshots/CSharpCodeBuilderTests.GenerateWithDifferentFormats_Should_MatchSnapshots_02b3255120757173.verified.txt +++ b/tests/NetEvolve.CodeBuilder.Tests.Integration/_snapshots/CSharpCodeBuilderTests.GenerateWithDifferentFormats_Should_MatchSnapshots_02b3255120757173.verified.txt @@ -1,4 +1,4 @@ public class TestClass { public void Method() { } -} \ No newline at end of file +} diff --git a/tests/NetEvolve.CodeBuilder.Tests.Integration/_snapshots/CSharpCodeBuilderTests.GenerateWithDifferentFormats_Should_MatchSnapshots_b6b4547aad375c78.verified.txt b/tests/NetEvolve.CodeBuilder.Tests.Integration/_snapshots/CSharpCodeBuilderTests.GenerateWithDifferentFormats_Should_MatchSnapshots_b6b4547aad375c78.verified.txt index 76924e7..328b349 100644 --- a/tests/NetEvolve.CodeBuilder.Tests.Integration/_snapshots/CSharpCodeBuilderTests.GenerateWithDifferentFormats_Should_MatchSnapshots_b6b4547aad375c78.verified.txt +++ b/tests/NetEvolve.CodeBuilder.Tests.Integration/_snapshots/CSharpCodeBuilderTests.GenerateWithDifferentFormats_Should_MatchSnapshots_b6b4547aad375c78.verified.txt @@ -1,4 +1,4 @@ public class TestClass { public void Method() { } -} \ No newline at end of file +} diff --git a/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.Append.cs b/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.Append.cs index 282770b..aeaec1c 100644 --- a/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.Append.cs +++ b/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.Append.cs @@ -330,14 +330,16 @@ public async Task Append_String_ClosingBrace_Should_Decrement_Indent_And_Append_ _ = await Assert .That(builder.ToString()) - .IsEqualTo("{" + Environment.NewLine + " test" + Environment.NewLine + "}"); + .IsEqualTo("{" + Environment.NewLine + " test" + Environment.NewLine + "}" + Environment.NewLine); // Verify indent is decremented for next content _ = builder.Append("after"); _ = await Assert .That(builder.ToString()) - .IsEqualTo("{" + Environment.NewLine + " test" + Environment.NewLine + "}" + "after"); + .IsEqualTo( + "{" + Environment.NewLine + " test" + Environment.NewLine + "}" + Environment.NewLine + "after" + ); } [Test] @@ -353,14 +355,16 @@ public async Task Append_String_ClosingBracket_Should_Decrement_Indent_And_Appen _ = await Assert .That(builder.ToString()) - .IsEqualTo("[" + Environment.NewLine + " test" + Environment.NewLine + "]"); + .IsEqualTo("[" + Environment.NewLine + " test" + Environment.NewLine + "]" + Environment.NewLine); // Verify indent is decremented for next content _ = builder.Append("after"); _ = await Assert .That(builder.ToString()) - .IsEqualTo("[" + Environment.NewLine + " test" + Environment.NewLine + "]" + "after"); + .IsEqualTo( + "[" + Environment.NewLine + " test" + Environment.NewLine + "]" + Environment.NewLine + "after" + ); } // New tests for missing branches in Char Append method @@ -424,4 +428,66 @@ public async Task Append_Char_OpeningBracket_Should_Increment_Indent_And_Append_ _ = await Assert.That(builder.ToString()).IsEqualTo("[" + Environment.NewLine + " test"); } + + [Test] + public async Task Append_ReadOnlySpan_NonEmpty_Should_AppendContent() + { + var builder = new CSharpCodeBuilder(); + + _ = builder.Append("hello".AsSpan()); + + _ = await Assert.That(builder.ToString()).IsEqualTo("hello"); + } + + [Test] + public async Task Append_ReadOnlySpan_Empty_Should_NotAppend() + { + var builder = new CSharpCodeBuilder(); + + _ = builder.Append(ReadOnlySpan.Empty); + + _ = await Assert.That(builder.ToString()).IsEqualTo(string.Empty); + } + + [Test] + public async Task Append_ReadOnlySpan_Should_Return_Same_Instance() + { + var builder = new CSharpCodeBuilder(); + + var result = builder.Append("hello".AsSpan()); + + _ = await Assert.That(result).IsEqualTo(builder); + } + + [Test] + public async Task Append_ReadOnlySpan_With_StartIndex_And_Count_Should_AppendSubspan() + { + var builder = new CSharpCodeBuilder(); + + _ = builder.Append("hello".AsSpan(), 1, 3); + + _ = await Assert.That(builder.ToString()).IsEqualTo("ell"); + } + + [Test] + public async Task Append_ReadOnlySpan_Empty_With_Indices_Should_NotAppend() + { + var builder = new CSharpCodeBuilder(); + + _ = builder.Append(ReadOnlySpan.Empty, 0, 0); + + _ = await Assert.That(builder.ToString()).IsEqualTo(string.Empty); + } + + [Test] + public async Task Append_ReadOnlySpan_With_Indentation_Should_Apply_Indentation() + { + var builder = new CSharpCodeBuilder(); + builder.IncrementIndent(); + _ = builder.AppendLine(); + + _ = builder.Append("hello".AsSpan()); + + _ = await Assert.That(builder.ToString()).IsEqualTo(Environment.NewLine + " hello"); + } } diff --git a/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.AppendFormat.cs b/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.AppendFormat.cs index a8eba50..7bd20d5 100644 --- a/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.AppendFormat.cs +++ b/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.AppendFormat.cs @@ -1,6 +1,7 @@ -namespace NetEvolve.CodeBuilder.Tests.Unit; +namespace NetEvolve.CodeBuilder.Tests.Unit; using System; +using System.Diagnostics.CodeAnalysis; using System.Globalization; public partial class CSharpCodeBuilderTests @@ -413,4 +414,193 @@ public async Task AppendFormat_WithComplexFormatting_Should_Format_Correctly() _ = await Assert.That(builder.ToString()).IsEqualTo("Date: 2023-01-15, Value: 000000FF"); } + + // AppendFormat(string format, object? arg0) — shortcut overload (uses InvariantCulture implicitly) + // CA1305 is intentionally suppressed for this block: the purpose of these tests is to verify the + // no-provider overloads resolve correctly and delegate to InvariantCulture. +#pragma warning disable CA1305 + + [Test] + public async Task AppendFormat_SingleArg_NoProvider_Should_Format_Correctly() + { + var builder = new CSharpCodeBuilder(10); + + _ = builder.AppendFormat("Value: {0}", 42); + + _ = await Assert.That(builder.ToString()).IsEqualTo("Value: 42"); + } + + [Test] + public async Task AppendFormat_SingleArg_NoProvider_Should_Use_InvariantCulture() + { + var builder = new CSharpCodeBuilder(10); + + _ = builder.AppendFormat("Value: {0:N2}", 1234.56m); + + // InvariantCulture uses period as decimal separator + _ = await Assert.That(builder.ToString()).IsEqualTo("Value: 1,234.56"); + } + + [Test] + public async Task AppendFormat_SingleArg_NoProvider_Should_Return_Same_Instance() + { + var builder = new CSharpCodeBuilder(10); + + var result = builder.AppendFormat("Value: {0}", 42); + + _ = await Assert.That(result).IsEqualTo(builder); + } + + [Test] + public async Task AppendFormat_SingleArg_NoProvider_Should_Apply_Indentation() + { + var builder = new CSharpCodeBuilder(10); + builder.IncrementIndent(); + + _ = builder.AppendLine().AppendFormat("Value: {0}", 42); + + _ = await Assert.That(builder.ToString()).IsEqualTo(Environment.NewLine + " Value: 42"); + } + + [Test] + public async Task AppendFormat_SingleArg_NoProvider_Null_Format_Should_Throw_ArgumentNullException() + { + var exception = Assert.Throws(() => + { + var builder = new CSharpCodeBuilder(10); + _ = builder.AppendFormat(null!, 42); + }); + + _ = await Assert.That(exception.ParamName).IsEqualTo("format"); + } + + [Test] + public async Task AppendFormat_SingleArg_NoProvider_Invalid_Format_Should_Throw_FormatException() => + await Assert.ThrowsAsync(() => + { + var builder = new CSharpCodeBuilder(10); + _ = builder.AppendFormat("Value: {1}", 42); + return Task.CompletedTask; + }); + + [Test] + public async Task AppendFormat_SingleArg_NoProvider_Null_Arg_Should_Format_As_Empty() + { + var builder = new CSharpCodeBuilder(10); + + _ = builder.AppendFormat("Value: [{0}]", (object?)null); + + _ = await Assert.That(builder.ToString()).IsEqualTo("Value: []"); + } + + [Test] + public async Task AppendFormat_ParamsArgs_NoProvider_Should_Format_Correctly() + { + var builder = new CSharpCodeBuilder(10); + + _ = builder.AppendFormat("Values: {0}, {1}", 42, "test"); + + _ = await Assert.That(builder.ToString()).IsEqualTo("Values: 42, test"); + } + + [Test] + public async Task AppendFormat_ParamsArgs_NoProvider_Should_Use_InvariantCulture() + { + var builder = new CSharpCodeBuilder(10); + + _ = builder.AppendFormat("Values: {0:N2}, {1}", 1234.56m, "test"); + + _ = await Assert.That(builder.ToString()).IsEqualTo("Values: 1,234.56, test"); + } + + [Test] + public async Task AppendFormat_ParamsArgs_NoProvider_Should_Return_Same_Instance() + { + var builder = new CSharpCodeBuilder(10); + + var result = builder.AppendFormat("Values: {0}, {1}", 42, "test"); + + _ = await Assert.That(result).IsEqualTo(builder); + } + + [Test] + public async Task AppendFormat_ParamsArgs_NoProvider_Should_Apply_Indentation() + { + var builder = new CSharpCodeBuilder(10); + builder.IncrementIndent(); + + _ = builder.AppendLine().AppendFormat("Values: {0}, {1}", 42, "test"); + + _ = await Assert.That(builder.ToString()).IsEqualTo(Environment.NewLine + " Values: 42, test"); + } + + [Test] + public async Task AppendFormat_ParamsArgs_NoProvider_Null_Format_Should_Throw_ArgumentNullException() + { + var exception = Assert.Throws(() => + { + var builder = new CSharpCodeBuilder(10); + _ = builder.AppendFormat(null!, 42, "test"); + }); + + _ = await Assert.That(exception.ParamName).IsEqualTo("format"); + } + + [Test] + [SuppressMessage( + "Globalization", + "CA1305:Specify IFormatProvider", + Justification = "Intentionally testing the no-provider overload." + )] + public async Task AppendFormat_ParamsArgs_NoProvider_Invalid_Format_Should_Throw_FormatException() => + await Assert.ThrowsAsync(() => + { + var builder = new CSharpCodeBuilder(10); + _ = builder.AppendFormat("Values: {0}, {2}", 42, "test"); + return Task.CompletedTask; + }); + + [Test] + [SuppressMessage( + "Globalization", + "CA1305:Specify IFormatProvider", + Justification = "Intentionally testing the no-provider overload." + )] + public async Task AppendFormat_ParamsArgs_NoProvider_Three_Args_Should_Format_Correctly() + { + var builder = new CSharpCodeBuilder(10); + + _ = builder.AppendFormat("Values: {0}, {1}, {2}", 42, "test", true); + + _ = await Assert.That(builder.ToString()).IsEqualTo("Values: 42, test, True"); + } + + [Test] + public async Task AppendFormat_ParamsArgs_NoProvider_With_Format_Specifier_Should_Format_Correctly() + { + var builder = new CSharpCodeBuilder(10); + var date = new DateTime(2023, 6, 15, 0, 0, 0, DateTimeKind.Utc); + + _ = builder.AppendFormat("{0:yyyy-MM-dd}: value={1:X2}", date, 255); + + _ = await Assert.That(builder.ToString()).IsEqualTo("2023-06-15: value=FF"); + } + + [Test] + [SuppressMessage( + "Globalization", + "CA1305:Specify IFormatProvider", + Justification = "Intentionally testing the no-provider overload." + )] + public async Task AppendFormat_ParamsArgs_NoProvider_Single_Element_Array_Should_Format_Correctly() + { + var builder = new CSharpCodeBuilder(10); + object?[] args = [42]; + + _ = builder.AppendFormat("Value: {0}", args); + + _ = await Assert.That(builder.ToString()).IsEqualTo("Value: 42"); + } + +#pragma warning restore CA1305 } diff --git a/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.AppendIf.cs b/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.AppendIf.cs index f2675d7..cacf52c 100644 --- a/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.AppendIf.cs +++ b/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.AppendIf.cs @@ -329,6 +329,66 @@ public async Task AppendIf_ReadOnlyMemory_With_StartIndex_And_Count_Should_Retur _ = await Assert.That(result).IsEqualTo(builder); } + [Test] + public async Task AppendIf_ReadOnlySpan_Condition_True_Should_Append_Characters() + { + var builder = new CSharpCodeBuilder(10); + + _ = builder.AppendIf(true, "hello".AsSpan()); + + _ = await Assert.That(builder.ToString()).IsEqualTo("hello"); + } + + [Test] + public async Task AppendIf_ReadOnlySpan_Condition_False_Should_Not_Append() + { + var builder = new CSharpCodeBuilder(10); + + _ = builder.AppendIf(false, "hello".AsSpan()); + + _ = await Assert.That(builder.ToString()).IsEqualTo(string.Empty); + } + + [Test] + public async Task AppendIf_ReadOnlySpan_Empty_Condition_True_Should_Not_Append() + { + var builder = new CSharpCodeBuilder(10); + + _ = builder.AppendIf(true, ReadOnlySpan.Empty); + + _ = await Assert.That(builder.ToString()).IsEqualTo(string.Empty); + } + + [Test] + public async Task AppendIf_ReadOnlySpan_Should_Return_Same_Instance() + { + var builder = new CSharpCodeBuilder(10); + + var result = builder.AppendIf(true, "hello".AsSpan()); + + _ = await Assert.That(result).IsEqualTo(builder); + } + + [Test] + public async Task AppendIf_ReadOnlySpan_With_StartIndex_And_Count_Condition_True_Should_Append_Subspan() + { + var builder = new CSharpCodeBuilder(10); + + _ = builder.AppendIf(true, "hello".AsSpan(), 1, 3); + + _ = await Assert.That(builder.ToString()).IsEqualTo("ell"); + } + + [Test] + public async Task AppendIf_ReadOnlySpan_With_StartIndex_And_Count_Condition_False_Should_Not_Append() + { + var builder = new CSharpCodeBuilder(10); + + _ = builder.AppendIf(false, "hello".AsSpan(), 1, 3); + + _ = await Assert.That(builder.ToString()).IsEqualTo(string.Empty); + } + [Test] public async Task AppendIf_String_Condition_True_Should_Append_Characters() { diff --git a/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.AppendLine.cs b/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.AppendLine.cs index b9d79fd..bbbcb08 100644 --- a/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.AppendLine.cs +++ b/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.AppendLine.cs @@ -256,4 +256,54 @@ public async Task AppendLine_Unsafe_CharPointer_Negative_Length_Should_Append_On _ = await Assert.That(result).IsEqualTo(builder); _ = await Assert.That(builderResult).IsEqualTo(Environment.NewLine); } + + [Test] + public async Task AppendLine_ReadOnlySpan_NonEmpty_Should_AppendContentWithNewline() + { + var builder = new CSharpCodeBuilder(); + + _ = builder.AppendLine("hello".AsSpan()); + + _ = await Assert.That(builder.ToString()).IsEqualTo("hello" + Environment.NewLine); + } + + [Test] + public async Task AppendLine_ReadOnlySpan_Empty_Should_AppendOnlyNewline() + { + var builder = new CSharpCodeBuilder(); + + _ = builder.AppendLine(ReadOnlySpan.Empty); + + _ = await Assert.That(builder.ToString()).IsEqualTo(Environment.NewLine); + } + + [Test] + public async Task AppendLine_ReadOnlySpan_Should_Return_Same_Instance() + { + var builder = new CSharpCodeBuilder(); + + var result = builder.AppendLine("hello".AsSpan()); + + _ = await Assert.That(result).IsEqualTo(builder); + } + + [Test] + public async Task AppendLine_ReadOnlySpan_With_StartIndex_And_Count_Should_AppendSubspanWithNewline() + { + var builder = new CSharpCodeBuilder(); + + _ = builder.AppendLine("hello".AsSpan(), 1, 3); + + _ = await Assert.That(builder.ToString()).IsEqualTo("ell" + Environment.NewLine); + } + + [Test] + public async Task AppendLine_ReadOnlySpan_Empty_With_Indices_Should_AppendOnlyNewline() + { + var builder = new CSharpCodeBuilder(); + + _ = builder.AppendLine(ReadOnlySpan.Empty, 0, 0); + + _ = await Assert.That(builder.ToString()).IsEqualTo(Environment.NewLine); + } } diff --git a/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.AppendLineIf.cs b/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.AppendLineIf.cs index 605a41e..8ede0b6 100644 --- a/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.AppendLineIf.cs +++ b/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.AppendLineIf.cs @@ -491,4 +491,64 @@ public async Task AppendLineIf_Mixed_With_AppendIf_Should_Work_Correctly() _ = await Assert.That(builder.ToString()).IsEqualTo(expected); } + + [Test] + public async Task AppendLineIf_ReadOnlySpan_Condition_True_Should_Append_With_Newline() + { + var builder = new CSharpCodeBuilder(10); + + _ = builder.AppendLineIf(true, "hello".AsSpan()); + + _ = await Assert.That(builder.ToString()).IsEqualTo("hello" + Environment.NewLine); + } + + [Test] + public async Task AppendLineIf_ReadOnlySpan_Condition_False_Should_Not_Append() + { + var builder = new CSharpCodeBuilder(10); + + _ = builder.AppendLineIf(false, "hello".AsSpan()); + + _ = await Assert.That(builder.ToString()).IsEqualTo(string.Empty); + } + + [Test] + public async Task AppendLineIf_ReadOnlySpan_Empty_Condition_True_Should_Append_Only_Newline() + { + var builder = new CSharpCodeBuilder(10); + + _ = builder.AppendLineIf(true, ReadOnlySpan.Empty); + + _ = await Assert.That(builder.ToString()).IsEqualTo(Environment.NewLine); + } + + [Test] + public async Task AppendLineIf_ReadOnlySpan_Should_Return_Same_Instance() + { + var builder = new CSharpCodeBuilder(10); + + var result = builder.AppendLineIf(true, "hello".AsSpan()); + + _ = await Assert.That(result).IsEqualTo(builder); + } + + [Test] + public async Task AppendLineIf_ReadOnlySpan_With_StartIndex_And_Count_Condition_True_Should_Append_Subspan() + { + var builder = new CSharpCodeBuilder(10); + + _ = builder.AppendLineIf(true, "hello".AsSpan(), 1, 3); + + _ = await Assert.That(builder.ToString()).IsEqualTo("ell" + Environment.NewLine); + } + + [Test] + public async Task AppendLineIf_ReadOnlySpan_With_StartIndex_And_Count_Condition_False_Should_Not_Append() + { + var builder = new CSharpCodeBuilder(10); + + _ = builder.AppendLineIf(false, "hello".AsSpan(), 1, 3); + + _ = await Assert.That(builder.ToString()).IsEqualTo(string.Empty); + } } diff --git a/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.Clear.cs b/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.Clear.cs index c30b81a..4f3db56 100644 --- a/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.Clear.cs +++ b/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.Clear.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.CodeBuilder.Tests.Unit; +namespace NetEvolve.CodeBuilder.Tests.Unit; using System; @@ -95,22 +95,22 @@ public async Task Clear_Should_Preserve_Capacity() } [Test] - public async Task Intend_Should_Append_Single_Indentation() + public async Task Indent_Should_Append_Single_Indentation() { var builder = new CSharpCodeBuilder(); - _ = builder.Intend().Append("Hello"); + _ = builder.Indent().Append("Hello"); var result = builder.ToString(); _ = await Assert.That(result).IsEqualTo(" Hello"); } [Test] - public async Task Intend_Should_Not_Affect_Indentation_Level() + public async Task Indent_Should_Not_Affect_Indentation_Level() { var builder = new CSharpCodeBuilder(); - _ = builder.Intend().AppendLine("First"); + _ = builder.Indent().AppendLine("First"); _ = builder.Append("Second"); var result = builder.ToString(); @@ -119,89 +119,89 @@ public async Task Intend_Should_Not_Affect_Indentation_Level() } [Test] - public async Task Intend_With_Tabs_Should_Append_Tab_Character() + public async Task Indent_With_Tabs_Should_Append_Tab_Character() { var builder = new CSharpCodeBuilder { UseTabs = true }; - _ = builder.Intend().Append("Hello"); + _ = builder.Indent().Append("Hello"); var result = builder.ToString(); _ = await Assert.That(result).IsEqualTo("\tHello"); } [Test] - public async Task Intend_With_Spaces_Should_Append_Four_Spaces() + public async Task Indent_With_Spaces_Should_Append_Four_Spaces() { var builder = new CSharpCodeBuilder { UseTabs = false }; - _ = builder.Intend().Append("Hello"); + _ = builder.Indent().Append("Hello"); var result = builder.ToString(); _ = await Assert.That(result).IsEqualTo(" Hello"); } [Test] - public async Task Intend_Multiple_Calls_Should_Append_Multiple_Indentations() + public async Task Indent_Multiple_Calls_Should_Append_Multiple_Indentations() { var builder = new CSharpCodeBuilder(); - _ = builder.Intend().Intend().Intend().Append("Hello"); + _ = builder.Indent().Indent().Indent().Append("Hello"); var result = builder.ToString(); _ = await Assert.That(result).IsEqualTo(" Hello"); // 12 spaces (3 * 4) } [Test] - public async Task Intend_Multiple_With_Tabs_Should_Append_Multiple_Tabs() + public async Task Indent_Multiple_With_Tabs_Should_Append_Multiple_Tabs() { var builder = new CSharpCodeBuilder { UseTabs = true }; - _ = builder.Intend().Intend().Intend().Append("Hello"); + _ = builder.Indent().Indent().Indent().Append("Hello"); var result = builder.ToString(); _ = await Assert.That(result).IsEqualTo("\t\t\tHello"); } [Test] - public async Task Intend_Should_Return_Builder_For_Chaining() + public async Task Indent_Should_Return_Builder_For_Chaining() { var builder = new CSharpCodeBuilder(); - var result = builder.Intend(); + var result = builder.Indent(); _ = await Assert.That(result).IsEqualTo(builder); } [Test] - public async Task Intend_In_Middle_Of_Line_Should_Append_Indentation() + public async Task Indent_In_Middle_Of_Line_Should_Append_Indentation() { var builder = new CSharpCodeBuilder(); - _ = builder.Append("Hello").Intend().Append("World"); + _ = builder.Append("Hello").Indent().Append("World"); var result = builder.ToString(); _ = await Assert.That(result).IsEqualTo("Hello World"); } [Test] - public async Task Intend_After_NewLine_Should_Add_Manual_Indentation() + public async Task Indent_After_NewLine_Should_Add_Manual_Indentation() { var builder = new CSharpCodeBuilder(); _ = builder.AppendLine("First"); - _ = builder.Intend().Append("Second"); + _ = builder.Indent().Append("Second"); var result = builder.ToString(); _ = await Assert.That(result).IsEqualTo("First" + Environment.NewLine + " Second"); } [Test] - public async Task Intend_With_Automatic_Indentation_Should_Stack() + public async Task Indent_With_Automatic_Indentation_Should_Stack() { var builder = new CSharpCodeBuilder(); builder.IncrementIndent(); // Set automatic indentation level to 1 - _ = builder.AppendLine().Intend().Append("Hello"); + _ = builder.AppendLine().Indent().Append("Hello"); var result = builder.ToString(); // Should have both automatic (4 spaces) and manual (4 spaces) indentation @@ -209,80 +209,105 @@ public async Task Intend_With_Automatic_Indentation_Should_Stack() } [Test] - public async Task Intend_Multiple_Mixed_With_Content_Should_Work() + public async Task Indent_Multiple_Mixed_With_Content_Should_Work() { var builder = new CSharpCodeBuilder(); _ = builder - .Intend() + .Indent() .Append("Level 1") .AppendLine() - .Intend() - .Intend() + .Indent() + .Indent() .Append("Level 2") .AppendLine() - .Intend() - .Intend() - .Intend() + .Indent() + .Indent() + .Indent() .Append("Level 3"); var result = builder.ToString(); - _ = await Assert.That(result).Contains(" Level 1"); - _ = await Assert.That(result).Contains(" Level 2"); - _ = await Assert.That(result).Contains(" Level 3"); + _ = await Assert + .That(result) + .IsEqualTo( + " Level 1" + Environment.NewLine + " Level 2" + Environment.NewLine + " Level 3" + ); } [Test] - public async Task Intend_Should_Work_With_Empty_Builder() + public async Task Indent_Should_Work_With_Empty_Builder() { var builder = new CSharpCodeBuilder(); - _ = builder.Intend(); + _ = builder.Indent(); var result = builder.ToString(); _ = await Assert.That(result).IsEqualTo(" "); } [Test] - public async Task Intend_Should_Work_After_Clear() + public async Task Clear_Should_Reset_IsNewline_State() + { + var builder = new CSharpCodeBuilder(); + _ = builder.Append("text"); // sets _isNewline to false + + _ = builder.Clear(); // must reset _isNewline to true + builder.IncrementIndent(); + _ = builder.Append("indented"); + + var result = builder.ToString(); + _ = await Assert.That(result).IsEqualTo(" indented"); + } + + [Test] + public async Task Indent_Should_Work_After_Clear() { var builder = new CSharpCodeBuilder(); _ = builder.Append("Hello"); _ = builder.Clear(); - _ = builder.Intend().Append("World"); + _ = builder.Indent().Append("World"); var result = builder.ToString(); _ = await Assert.That(result).IsEqualTo(" World"); } [Test] - public async Task Intend_Combined_With_Scope_Should_Add_Extra_Indentation() + public async Task Indent_Combined_With_Scope_Should_Add_Extra_Indentation() { var builder = new CSharpCodeBuilder(); using (builder.Scope()) { - _ = builder.Intend().Append("Extra indented"); + _ = builder.Indent().Append("Extra indented"); } - var result = builder.ToString(); - // Should have both scope indentation (4 spaces) and manual indentation (4 spaces) - _ = await Assert.That(result).Contains(" Extra indented"); // 8 spaces + var result = builder.ToString().Replace("\r\n", "\n", StringComparison.Ordinal); + var expected = """ + { + Extra indented} + + """.Replace("\r\n", "\n", StringComparison.Ordinal); + _ = await Assert.That(result).IsEqualTo(expected); } [Test] - public async Task Intend_Combined_With_ScopeLine_Should_Add_Extra_Indentation() + public async Task Indent_Combined_With_ScopeLine_Should_Add_Extra_Indentation() { var builder = new CSharpCodeBuilder(); using (builder.ScopeLine("public class MyClass")) { - _ = builder.Intend().Append("// Extra indented comment"); + _ = builder.Indent().Append("// Extra indented comment"); } - var result = builder.ToString(); - // Should have both scope indentation and manual indentation - _ = await Assert.That(result).Contains(" // Extra indented comment"); // 8 spaces + var result = builder.ToString().Replace("\r\n", "\n", StringComparison.Ordinal); + var expected = """ + public class MyClass + { + // Extra indented comment} + + """.Replace("\r\n", "\n", StringComparison.Ordinal); + _ = await Assert.That(result).IsEqualTo(expected); } } diff --git a/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.Documentation.cs b/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.Documentation.cs index d5e2b54..39f8891 100644 --- a/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.Documentation.cs +++ b/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.Documentation.cs @@ -44,15 +44,17 @@ public async Task AppendXmlDocSummary_WithSingleLine_Should_AppendSummaryElement { var builder = new CSharpCodeBuilder(); - var result = builder.AppendXmlDocSummary("This is a summary").ToString(); + var result = builder + .AppendXmlDocSummary("This is a summary") + .ToString() + .Replace("\r\n", "\n", StringComparison.Ordinal); - var expected = - "/// " - + Environment.NewLine - + "/// This is a summary" - + Environment.NewLine - + "/// " - + Environment.NewLine; + var expected = """ + /// + /// This is a summary + /// + + """.Replace("\r\n", "\n", StringComparison.Ordinal); _ = await Assert.That(result).IsEqualTo(expected); } @@ -63,19 +65,19 @@ public async Task AppendXmlDocSummary_WithMultipleLines_Should_AppendSummaryElem var builder = new CSharpCodeBuilder(); var summaryLines = new[] { "First line", "Second line", "Third line" }; - var result = builder.AppendXmlDocSummary(summaryLines).ToString(); + var result = builder + .AppendXmlDocSummary(summaryLines) + .ToString() + .Replace("\r\n", "\n", StringComparison.Ordinal); - var expected = - "/// " - + Environment.NewLine - + "/// First line" - + Environment.NewLine - + "/// Second line" - + Environment.NewLine - + "/// Third line" - + Environment.NewLine - + "/// " - + Environment.NewLine; + var expected = """ + /// + /// First line + /// Second line + /// Third line + /// + + """.Replace("\r\n", "\n", StringComparison.Ordinal); _ = await Assert.That(result).IsEqualTo(expected); } @@ -86,21 +88,45 @@ public async Task AppendXmlDocSummary_WithEmptyAndNullLines_Should_SkipEmptyLine var builder = new CSharpCodeBuilder(); var summaryLines = new List { "First line", "", null, "Last line" }; - var result = builder.AppendXmlDocSummary(summaryLines.Where(x => x is not null)!).ToString(); + var result = builder + .AppendXmlDocSummary(summaryLines.Where(x => x is not null)!) + .ToString() + .Replace("\r\n", "\n", StringComparison.Ordinal); - var expected = - "/// " - + Environment.NewLine - + "/// First line" - + Environment.NewLine - + "/// Last line" - + Environment.NewLine - + "/// " - + Environment.NewLine; + var expected = """ + /// + /// First line + /// Last line + /// + + """.Replace("\r\n", "\n", StringComparison.Ordinal); + + _ = await Assert.That(result).IsEqualTo(expected); _ = await Assert.That(result).IsEqualTo(expected); } + [Test] + public async Task AppendXmlDocSummary_WithOnlyEmptyLines_Should_NotAppendAnything() + { + var builder = new CSharpCodeBuilder(); + var summaryLines = new[] { "", "", "" }; + + var result = builder.AppendXmlDocSummary(summaryLines).ToString(); + + _ = await Assert.That(result).IsEqualTo(string.Empty); + } + + [Test] + public async Task AppendXmlDocSummary_WithEmptyCollection_Should_NotAppendAnything() + { + var builder = new CSharpCodeBuilder(); + + var result = builder.AppendXmlDocSummary(Array.Empty()).ToString(); + + _ = await Assert.That(result).IsEqualTo(string.Empty); + } + [Test] public async Task AppendXmlDocParam_WithValidParameters_Should_AppendParamElement() { @@ -135,13 +161,13 @@ public async Task AppendXmlDocParams_WithMultipleParameters_Should_AppendAllPara ("param2", "Second parameter"), }; - var result = builder.AppendXmlDocParams(parameters).ToString(); + var result = builder.AppendXmlDocParams(parameters).ToString().Replace("\r\n", "\n", StringComparison.Ordinal); - var expected = - "/// First parameter" - + Environment.NewLine - + "/// Second parameter" - + Environment.NewLine; + var expected = """ + /// First parameter + /// Second parameter + + """.Replace("\r\n", "\n", StringComparison.Ordinal); _ = await Assert.That(result).IsEqualTo(expected); } @@ -164,15 +190,17 @@ public async Task AppendXmlDocRemarks_WithSingleLine_Should_AppendRemarksElement { var builder = new CSharpCodeBuilder(); - var result = builder.AppendXmlDocRemarks("This is a remark").ToString(); + var result = builder + .AppendXmlDocRemarks("This is a remark") + .ToString() + .Replace("\r\n", "\n", StringComparison.Ordinal); - var expected = - "/// " - + Environment.NewLine - + "/// This is a remark" - + Environment.NewLine - + "/// " - + Environment.NewLine; + var expected = """ + /// + /// This is a remark + /// + + """.Replace("\r\n", "\n", StringComparison.Ordinal); _ = await Assert.That(result).IsEqualTo(expected); } @@ -183,19 +211,19 @@ public async Task AppendXmlDocRemarks_WithMultipleLines_Should_AppendRemarksElem var builder = new CSharpCodeBuilder(); var remarksLines = new[] { "First remark line", "Second remark line", "Third remark line" }; - var result = builder.AppendXmlDocRemarks(remarksLines).ToString(); + var result = builder + .AppendXmlDocRemarks(remarksLines) + .ToString() + .Replace("\r\n", "\n", StringComparison.Ordinal); - var expected = - "/// " - + Environment.NewLine - + "/// First remark line" - + Environment.NewLine - + "/// Second remark line" - + Environment.NewLine - + "/// Third remark line" - + Environment.NewLine - + "/// " - + Environment.NewLine; + var expected = """ + /// + /// First remark line + /// Second remark line + /// Third remark line + /// + + """.Replace("\r\n", "\n", StringComparison.Ordinal); _ = await Assert.That(result).IsEqualTo(expected); } @@ -206,34 +234,31 @@ public async Task AppendXmlDocRemarks_WithEmptyAndNullLines_Should_SkipEmptyLine var builder = new CSharpCodeBuilder(); var remarksLines = new[] { "First remark", "", "Third remark" }; - var result = builder.AppendXmlDocRemarks(remarksLines).ToString(); + var result = builder + .AppendXmlDocRemarks(remarksLines) + .ToString() + .Replace("\r\n", "\n", StringComparison.Ordinal); - var expected = - "/// " - + Environment.NewLine - + "/// First remark" - + Environment.NewLine - + "/// Third remark" - + Environment.NewLine - + "/// " - + Environment.NewLine; + var expected = """ + /// + /// First remark + /// Third remark + /// + + """.Replace("\r\n", "\n", StringComparison.Ordinal); _ = await Assert.That(result).IsEqualTo(expected); } [Test] - public async Task AppendXmlDocRemarks_WithOnlyEmptyLines_Should_AppendOnlyOpeningTag() + public async Task AppendXmlDocRemarks_WithOnlyEmptyLines_Should_NotAppendAnything() { var builder = new CSharpCodeBuilder(); var remarksLines = new[] { "", "", "" }; var result = builder.AppendXmlDocRemarks(remarksLines).ToString(); - // The current implementation has a bug where it opens the tag but doesn't close it - // when there are no valid content lines - var expected = "/// " + Environment.NewLine; - - _ = await Assert.That(result).IsEqualTo(expected); + _ = await Assert.That(result).IsEqualTo(string.Empty); } [Test] @@ -247,18 +272,14 @@ public async Task AppendXmlDocRemarks_WithNullCollection_Should_NotAppendAnythin } [Test] - public async Task AppendXmlDocRemarks_WithEmptyCollection_Should_AppendOnlyOpeningTag() + public async Task AppendXmlDocRemarks_WithEmptyCollection_Should_NotAppendAnything() { var builder = new CSharpCodeBuilder(); var remarksLines = Array.Empty(); var result = builder.AppendXmlDocRemarks(remarksLines).ToString(); - // The current implementation has a bug where it opens the tag but doesn't close it - // when there are no valid content lines - var expected = "/// " + Environment.NewLine; - - _ = await Assert.That(result).IsEqualTo(expected); + _ = await Assert.That(result).IsEqualTo(string.Empty); } [Test] @@ -266,15 +287,17 @@ public async Task AppendXmlDocExample_WithContent_Should_AppendExampleElement() { var builder = new CSharpCodeBuilder(); - var result = builder.AppendXmlDocExample("var example = new Example();").ToString(); + var result = builder + .AppendXmlDocExample("var example = new Example();") + .ToString() + .Replace("\r\n", "\n", StringComparison.Ordinal); + + var expected = """ + /// + /// var example = new Example(); + /// - var expected = - "/// " - + Environment.NewLine - + "/// var example = new Example();" - + Environment.NewLine - + "/// " - + Environment.NewLine; + """.Replace("\r\n", "\n", StringComparison.Ordinal); _ = await Assert.That(result).IsEqualTo(expected); } @@ -290,19 +313,19 @@ public async Task AppendXmlDocExample_WithMultipleLines_Should_AppendExampleElem "var result = builder.ToString();", }; - var result = builder.AppendXmlDocExample(exampleLines).ToString(); + var result = builder + .AppendXmlDocExample(exampleLines) + .ToString() + .Replace("\r\n", "\n", StringComparison.Ordinal); - var expected = - "/// " - + Environment.NewLine - + "/// var builder = new CSharpCodeBuilder();" - + Environment.NewLine - + "/// builder.AppendLine(\"Hello World\");" - + Environment.NewLine - + "/// var result = builder.ToString();" - + Environment.NewLine - + "/// " - + Environment.NewLine; + var expected = """ + /// + /// var builder = new CSharpCodeBuilder(); + /// builder.AppendLine("Hello World"); + /// var result = builder.ToString(); + /// + + """.Replace("\r\n", "\n", StringComparison.Ordinal); _ = await Assert.That(result).IsEqualTo(expected); } @@ -313,21 +336,43 @@ public async Task AppendXmlDocExample_WithEmptyAndNullLines_Should_SkipEmptyLine var builder = new CSharpCodeBuilder(); var exampleLines = new[] { "var x = 1;", "", "var y = 2;" }; - var result = builder.AppendXmlDocExample(exampleLines).ToString(); + var result = builder + .AppendXmlDocExample(exampleLines) + .ToString() + .Replace("\r\n", "\n", StringComparison.Ordinal); - var expected = - "/// " - + Environment.NewLine - + "/// var x = 1;" - + Environment.NewLine - + "/// var y = 2;" - + Environment.NewLine - + "/// " - + Environment.NewLine; + var expected = """ + /// + /// var x = 1; + /// var y = 2; + /// + + """.Replace("\r\n", "\n", StringComparison.Ordinal); _ = await Assert.That(result).IsEqualTo(expected); } + [Test] + public async Task AppendXmlDocExample_WithOnlyEmptyLines_Should_NotAppendAnything() + { + var builder = new CSharpCodeBuilder(); + var exampleLines = new[] { "", "", "" }; + + var result = builder.AppendXmlDocExample(exampleLines).ToString(); + + _ = await Assert.That(result).IsEqualTo(string.Empty); + } + + [Test] + public async Task AppendXmlDocExample_WithEmptyCollection_Should_NotAppendAnything() + { + var builder = new CSharpCodeBuilder(); + + var result = builder.AppendXmlDocExample(Array.Empty()).ToString(); + + _ = await Assert.That(result).IsEqualTo(string.Empty); + } + [Test] public async Task AppendXmlDocSee_WithCref_Should_AppendSeeElement() { @@ -339,6 +384,107 @@ public async Task AppendXmlDocSee_WithCref_Should_AppendSeeElement() _ = await Assert.That(builder.ToString()).IsEqualTo("/// " + Environment.NewLine); } + [Test] + public async Task AppendXmlDocSee_WithNullCref_Should_NotAppendAnything() + { + var builder = new CSharpCodeBuilder(); + + _ = builder.AppendXmlDocSee(null); + + _ = await Assert.That(builder.ToString()).IsEqualTo(string.Empty); + } + + [Test] + public async Task AppendXmlDocSee_WithIsHrefTrue_Should_UseHrefAttribute() + { + var builder = new CSharpCodeBuilder(); + + _ = builder.AppendXmlDocSee("https://example.com", isHref: true); + + _ = await Assert + .That(builder.ToString()) + .IsEqualTo("/// " + Environment.NewLine); + } + + [Test] + public async Task AppendXmlDocSeeAlso_WithCref_Should_AppendSeealsoElement() + { + var builder = new CSharpCodeBuilder(); + + var result = builder.AppendXmlDocSeeAlso("System.String"); + + _ = await Assert.That(result).IsEqualTo(builder); + _ = await Assert + .That(builder.ToString()) + .IsEqualTo("/// " + Environment.NewLine); + } + + [Test] + public async Task AppendXmlDocSeeAlso_WithNullCref_Should_NotAppendAnything() + { + var builder = new CSharpCodeBuilder(); + + _ = builder.AppendXmlDocSeeAlso(null); + + _ = await Assert.That(builder.ToString()).IsEqualTo(string.Empty); + } + + [Test] + public async Task AppendXmlDocSeeAlso_WithEmptyCref_Should_NotAppendAnything() + { + var builder = new CSharpCodeBuilder(); + + _ = builder.AppendXmlDocSeeAlso(string.Empty); + + _ = await Assert.That(builder.ToString()).IsEqualTo(string.Empty); + } + + [Test] + public async Task AppendXmlDocSeeAlso_WithIsHrefTrue_Should_UseHrefAttribute() + { + var builder = new CSharpCodeBuilder(); + + _ = builder.AppendXmlDocSeeAlso("https://example.com", isHref: true); + + _ = await Assert + .That(builder.ToString()) + .IsEqualTo("/// " + Environment.NewLine); + } + + [Test] + public async Task AppendXmlDocSeeAlso_WithIsHrefFalse_Should_UseCrefAttribute() + { + var builder = new CSharpCodeBuilder(); + + _ = builder.AppendXmlDocSeeAlso("MyNamespace.MyClass", isHref: false); + + _ = await Assert + .That(builder.ToString()) + .IsEqualTo("/// " + Environment.NewLine); + } + + [Test] + public async Task AppendXmlDocSeeAlso_AfterContent_Should_StartOnNewLine() + { + var builder = new CSharpCodeBuilder(); + + var result = builder + .AppendXmlDocSummary("Method summary") + .AppendXmlDocSeeAlso("RelatedClass") + .ToString() + .Replace("\r\n", "\n", StringComparison.Ordinal); + + var expected = """ + /// + /// Method summary + /// + /// + + """.Replace("\r\n", "\n", StringComparison.Ordinal); + + _ = await Assert.That(result).IsEqualTo(expected); + } + [Test] public async Task AppendXmlDocValue_WithDescription_Should_AppendValueElement() { @@ -418,27 +564,21 @@ public async Task XmlDocumentationMethods_Should_SupportMethodChaining() .AppendXmlDocParam("param2", "Second parameter") .AppendXmlDocReturns("Return value description") .AppendXmlDocRemarks("Additional remarks") - .ToString(); - - var expected = - "/// " - + Environment.NewLine - + "/// Method summary" - + Environment.NewLine - + "/// " - + Environment.NewLine - + "/// First parameter" - + Environment.NewLine - + "/// Second parameter" - + Environment.NewLine - + "/// Return value description" - + Environment.NewLine - + "/// " - + Environment.NewLine - + "/// Additional remarks" - + Environment.NewLine - + "/// " - + Environment.NewLine; + .ToString() + .Replace("\r\n", "\n", StringComparison.Ordinal); + + var expected = """ + /// + /// Method summary + /// + /// First parameter + /// Second parameter + /// Return value description + /// + /// Additional remarks + /// + + """.Replace("\r\n", "\n", StringComparison.Ordinal); _ = await Assert.That(result).IsEqualTo(expected); } @@ -452,17 +592,16 @@ public async Task XmlDocumentationMethods_WithIndentation_Should_RespectIndentat var result = builder .AppendXmlDocSummary("Indented summary") .AppendXmlDocParam("param", "Parameter description") - .ToString(); + .ToString() + .Replace("\r\n", "\n", StringComparison.Ordinal); - var expected = - " /// " - + Environment.NewLine - + " /// Indented summary" - + Environment.NewLine - + " /// " - + Environment.NewLine - + " /// Parameter description" - + Environment.NewLine; + var expected = """ + /// + /// Indented summary + /// + /// Parameter description + + """.Replace("\r\n", "\n", StringComparison.Ordinal); _ = await Assert.That(result).IsEqualTo(expected); } @@ -477,20 +616,17 @@ public async Task XmlDocumentationMethods_AfterContent_Should_StartOnNewLine() .AppendXmlDocSummary("Method summary") .AppendXmlDocParam("param", "Parameter description") .Append("public void MyMethod(string param) { }") - .ToString(); - - var expected = - "public class MyClass" - + Environment.NewLine - + "/// " - + Environment.NewLine - + "/// Method summary" - + Environment.NewLine - + "/// " - + Environment.NewLine - + "/// Parameter description" - + Environment.NewLine - + "public void MyMethod(string param) { }"; + .ToString() + .Replace("\r\n", "\n", StringComparison.Ordinal); + + var expected = """ + public class MyClass + /// + /// Method summary + /// + /// Parameter description + public void MyMethod(string param) { } + """.Replace("\r\n", "\n", StringComparison.Ordinal); _ = await Assert.That(result).IsEqualTo(expected); } @@ -503,17 +639,16 @@ public async Task XmlDocumentationMethods_AtStartOfBuilder_Should_NotAddExtraNew var result = builder .AppendXmlDocSummary("Method summary") .AppendXmlDocParam("param", "Parameter description") - .ToString(); + .ToString() + .Replace("\r\n", "\n", StringComparison.Ordinal); - var expected = - "/// " - + Environment.NewLine - + "/// Method summary" - + Environment.NewLine - + "/// " - + Environment.NewLine - + "/// Parameter description" - + Environment.NewLine; + var expected = """ + /// + /// Method summary + /// + /// Parameter description + + """.Replace("\r\n", "\n", StringComparison.Ordinal); _ = await Assert.That(result).IsEqualTo(expected); } @@ -527,19 +662,17 @@ public async Task XmlDocumentationMethods_AfterNewLine_Should_NotAddExtraNewLine .AppendLine("public class MyClass") .AppendXmlDocSummary("Method summary") .AppendXmlDocParam("param", "Parameter description") - .ToString(); + .ToString() + .Replace("\r\n", "\n", StringComparison.Ordinal); - var expected = - "public class MyClass" - + Environment.NewLine - + "/// " - + Environment.NewLine - + "/// Method summary" - + Environment.NewLine - + "/// " - + Environment.NewLine - + "/// Parameter description" - + Environment.NewLine; + var expected = """ + public class MyClass + /// + /// Method summary + /// + /// Parameter description + + """.Replace("\r\n", "\n", StringComparison.Ordinal); _ = await Assert.That(result).IsEqualTo(expected); } @@ -644,15 +777,17 @@ public async Task AppendXmlDocExceptions_WithValidExceptions_Should_AppendAllExc ("ArgumentOutOfRangeException", "Thrown when value is out of range"), }; - var result = builder.AppendXmlDocExceptions(exceptions).ToString(); + var result = builder + .AppendXmlDocExceptions(exceptions) + .ToString() + .Replace("\r\n", "\n", StringComparison.Ordinal); - var expected = - "/// Thrown when argument is null" - + Environment.NewLine - + "/// Thrown when operation is invalid" - + Environment.NewLine - + "/// Thrown when value is out of range" - + Environment.NewLine; + var expected = """ + /// Thrown when argument is null + /// Thrown when operation is invalid + /// Thrown when value is out of range + + """.Replace("\r\n", "\n", StringComparison.Ordinal); _ = await Assert.That(result).IsEqualTo(expected); } @@ -710,13 +845,16 @@ public async Task AppendXmlDocExceptions_WithInvalidExceptions_Should_SkipInvali ("ValidException", "Another valid exception"), }; - var result = builder.AppendXmlDocExceptions(exceptions).ToString(); + var result = builder + .AppendXmlDocExceptions(exceptions) + .ToString() + .Replace("\r\n", "\n", StringComparison.Ordinal); - var expected = - "/// Valid exception" - + Environment.NewLine - + "/// Another valid exception" - + Environment.NewLine; + var expected = """ + /// Valid exception + /// Another valid exception + + """.Replace("\r\n", "\n", StringComparison.Ordinal); _ = await Assert.That(result).IsEqualTo(expected); } @@ -735,21 +873,18 @@ public async Task AppendXmlDocExceptions_Should_SupportMethodChaining() .AppendXmlDocSummary("Method summary") .AppendXmlDocExceptions(exceptions) .AppendXmlDocReturns("Return value") - .ToString(); + .ToString() + .Replace("\r\n", "\n", StringComparison.Ordinal); - var expected = - "/// " - + Environment.NewLine - + "/// Method summary" - + Environment.NewLine - + "/// " - + Environment.NewLine - + "/// Thrown when argument is null" - + Environment.NewLine - + "/// Thrown when operation is invalid" - + Environment.NewLine - + "/// Return value" - + Environment.NewLine; + var expected = """ + /// + /// Method summary + /// + /// Thrown when argument is null + /// Thrown when operation is invalid + /// Return value + + """.Replace("\r\n", "\n", StringComparison.Ordinal); _ = await Assert.That(result).IsEqualTo(expected); } @@ -765,13 +900,16 @@ public async Task AppendXmlDocExceptions_WithIndentation_Should_RespectIndentati ("InvalidOperationException", "Thrown when operation is invalid"), }; - var result = builder.AppendXmlDocExceptions(exceptions).ToString(); + var result = builder + .AppendXmlDocExceptions(exceptions) + .ToString() + .Replace("\r\n", "\n", StringComparison.Ordinal); - var expected = - " /// Thrown when argument is null" - + Environment.NewLine - + " /// Thrown when operation is invalid" - + Environment.NewLine; + var expected = """ + /// Thrown when argument is null + /// Thrown when operation is invalid + + """.Replace("\r\n", "\n", StringComparison.Ordinal); _ = await Assert.That(result).IsEqualTo(expected); } @@ -803,13 +941,16 @@ public async Task AppendXmlDocTypeParams_WithMultipleParameters_Should_AppendAll ("U", "Second type parameter"), }; - var result = builder.AppendXmlDocTypeParams(typeParameters).ToString(); + var result = builder + .AppendXmlDocTypeParams(typeParameters) + .ToString() + .Replace("\r\n", "\n", StringComparison.Ordinal); - var expected = - "/// First type parameter" - + Environment.NewLine - + "/// Second type parameter" - + Environment.NewLine; + var expected = """ + /// First type parameter + /// Second type parameter + + """.Replace("\r\n", "\n", StringComparison.Ordinal); _ = await Assert.That(result).IsEqualTo(expected); } @@ -862,13 +1003,16 @@ public async Task AppendXmlDocTypeParams_WithInvalidEntries_Should_SkipInvalidEn ("W", "Another valid type parameter"), }; - var result = builder.AppendXmlDocTypeParams(typeParameters).ToString(); + var result = builder + .AppendXmlDocTypeParams(typeParameters) + .ToString() + .Replace("\r\n", "\n", StringComparison.Ordinal); - var expected = - "/// Valid type parameter" - + Environment.NewLine - + "/// Another valid type parameter" - + Environment.NewLine; + var expected = """ + /// Valid type parameter + /// Another valid type parameter + + """.Replace("\r\n", "\n", StringComparison.Ordinal); _ = await Assert.That(result).IsEqualTo(expected); } @@ -904,21 +1048,18 @@ public async Task AppendXmlDocTypeParams_Should_SupportMethodChaining() .AppendXmlDocSummary("Generic class") .AppendXmlDocTypeParams(typeParameters) .AppendXmlDocReturns("Return value") - .ToString(); + .ToString() + .Replace("\r\n", "\n", StringComparison.Ordinal); - var expected = - "/// " - + Environment.NewLine - + "/// Generic class" - + Environment.NewLine - + "/// " - + Environment.NewLine - + "/// First type parameter" - + Environment.NewLine - + "/// Second type parameter" - + Environment.NewLine - + "/// Return value" - + Environment.NewLine; + var expected = """ + /// + /// Generic class + /// + /// First type parameter + /// Second type parameter + /// Return value + + """.Replace("\r\n", "\n", StringComparison.Ordinal); _ = await Assert.That(result).IsEqualTo(expected); } @@ -934,13 +1075,16 @@ public async Task AppendXmlDocTypeParams_WithIndentation_Should_RespectIndentati ("U", "Second type parameter"), }; - var result = builder.AppendXmlDocTypeParams(typeParameters).ToString(); + var result = builder + .AppendXmlDocTypeParams(typeParameters) + .ToString() + .Replace("\r\n", "\n", StringComparison.Ordinal); - var expected = - " /// First type parameter" - + Environment.NewLine - + " /// Second type parameter" - + Environment.NewLine; + var expected = """ + /// First type parameter + /// Second type parameter + + """.Replace("\r\n", "\n", StringComparison.Ordinal); _ = await Assert.That(result).IsEqualTo(expected); } diff --git a/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.Indentation.cs b/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.Indentation.cs index 8b0e745..bcafd26 100644 --- a/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.Indentation.cs +++ b/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.Indentation.cs @@ -146,8 +146,8 @@ public async Task IncrementIndent_And_DecrementIndent_Multiple_Operations_Should _ = builder.Append("public void Method()"); _ = builder.Append("{"); // This automatically increments indent and adds newline _ = builder.Append("Console.WriteLine(\"Hello\");"); - _ = builder.Append("}"); // This automatically decrements indent but doesn't add newline - _ = builder.Append("}"); // This automatically decrements indent but doesn't add newline + _ = builder.Append("}"); // This automatically decrements indent and moves to new line + _ = builder.Append("}"); // This automatically decrements indent and moves to new line var result = builder.ToString().Replace("\r\n", "\n", StringComparison.Ordinal); @@ -158,7 +158,8 @@ public void Method(){ Console.WriteLine("Hello"); } } - """; + + """.Replace("\r\n", "\n", StringComparison.Ordinal); _ = await Assert.That(result).IsEqualTo(expected); } diff --git a/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.Scope.cs b/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.Scope.cs index e730e8f..0b58c53 100644 --- a/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.Scope.cs +++ b/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.Scope.cs @@ -26,8 +26,13 @@ public async Task Scope_Should_Append_Opening_Brace() // Empty scope } - var result = builder.ToString(); - _ = await Assert.That(result).Contains("{"); + var result = builder.ToString().Replace("\r\n", "\n", StringComparison.Ordinal); + var expected = """ + { + } + + """.Replace("\r\n", "\n", StringComparison.Ordinal); + _ = await Assert.That(result).IsEqualTo(expected); } [Test] @@ -40,8 +45,13 @@ public async Task Scope_Should_Append_Closing_Brace_On_Dispose() // Empty scope } - var result = builder.ToString(); - _ = await Assert.That(result).Contains("}"); + var result = builder.ToString().Replace("\r\n", "\n", StringComparison.Ordinal); + var expected = """ + { + } + + """.Replace("\r\n", "\n", StringComparison.Ordinal); + _ = await Assert.That(result).IsEqualTo(expected); } [Test] @@ -54,9 +64,13 @@ public async Task Scope_Should_Contain_Both_Braces() // Empty scope } - var result = builder.ToString(); - _ = await Assert.That(result).Contains("{"); - _ = await Assert.That(result).Contains("}"); + var result = builder.ToString().Replace("\r\n", "\n", StringComparison.Ordinal); + var expected = """ + { + } + + """.Replace("\r\n", "\n", StringComparison.Ordinal); + _ = await Assert.That(result).IsEqualTo(expected); } [Test] @@ -73,9 +87,15 @@ public async Task Scope_Nested_Should_Create_Multiple_Indentation_Levels() } } - var result = builder.ToString(); - _ = await Assert.That(result).Contains(" Level 1"); // 4 spaces - _ = await Assert.That(result).Contains(" Level 2"); // 8 spaces + var result = builder.ToString().Replace("\r\n", "\n", StringComparison.Ordinal); + var expected = """ + { + Level 1{ + Level 2} + } + + """.Replace("\r\n", "\n", StringComparison.Ordinal); + _ = await Assert.That(result).IsEqualTo(expected); } [Test] @@ -93,10 +113,15 @@ public async Task Scope_Multiple_Sequential_Should_Reset_Indentation() _ = builder.Append("Second"); } - var result = builder.ToString(); - // Both should be at the same indentation level (top level) - _ = await Assert.That(result).Contains("{"); - _ = await Assert.That(result).Contains("}"); + var result = builder.ToString().Replace("\r\n", "\n", StringComparison.Ordinal); + var expected = """ + { + First} + { + Second} + + """.Replace("\r\n", "\n", StringComparison.Ordinal); + _ = await Assert.That(result).IsEqualTo(expected); } [Test] @@ -110,7 +135,7 @@ public async Task Scope_With_Tabs_Should_Use_Tab_Indentation() } var result = builder.ToString(); - _ = await Assert.That(result).Contains("\tHello"); + _ = await Assert.That(result).IsEqualTo("{" + Environment.NewLine + "\tHello}" + Environment.NewLine); } #endregion @@ -127,8 +152,14 @@ public async Task ScopeLine_Should_Append_Line_Before_Scope() // Empty scope } - var result = builder.ToString(); - _ = await Assert.That(result).Contains("public class MyClass"); + var result = builder.ToString().Replace("\r\n", "\n", StringComparison.Ordinal); + var expected = """ + public class MyClass + { + } + + """.Replace("\r\n", "\n", StringComparison.Ordinal); + _ = await Assert.That(result).IsEqualTo(expected); } [Test] @@ -141,8 +172,14 @@ public async Task ScopeLine_Should_Include_Opening_Brace() // Empty scope } - var result = builder.ToString(); - _ = await Assert.That(result).Contains("{"); + var result = builder.ToString().Replace("\r\n", "\n", StringComparison.Ordinal); + var expected = """ + public class MyClass + { + } + + """.Replace("\r\n", "\n", StringComparison.Ordinal); + _ = await Assert.That(result).IsEqualTo(expected); } [Test] @@ -155,8 +192,14 @@ public async Task ScopeLine_Should_Include_Closing_Brace_On_Dispose() // Empty scope } - var result = builder.ToString(); - _ = await Assert.That(result).Contains("}"); + var result = builder.ToString().Replace("\r\n", "\n", StringComparison.Ordinal); + var expected = """ + public class MyClass + { + } + + """.Replace("\r\n", "\n", StringComparison.Ordinal); + _ = await Assert.That(result).IsEqualTo(expected); } [Test] @@ -169,9 +212,14 @@ public async Task ScopeLine_With_Null_Value_Should_Append_Only_Braces() // Empty scope } - var result = builder.ToString(); - _ = await Assert.That(result).Contains("{"); - _ = await Assert.That(result).Contains("}"); + var result = builder.ToString().Replace("\r\n", "\n", StringComparison.Ordinal); + var expected = """ + + { + } + + """.Replace("\r\n", "\n", StringComparison.Ordinal); + _ = await Assert.That(result).IsEqualTo(expected); } [Test] @@ -184,9 +232,14 @@ public async Task ScopeLine_With_Empty_String_Should_Append_Only_Braces() // Empty scope } - var result = builder.ToString(); - _ = await Assert.That(result).Contains("{"); - _ = await Assert.That(result).Contains("}"); + var result = builder.ToString().Replace("\r\n", "\n", StringComparison.Ordinal); + var expected = """ + + { + } + + """.Replace("\r\n", "\n", StringComparison.Ordinal); + _ = await Assert.That(result).IsEqualTo(expected); } [Test] @@ -199,9 +252,14 @@ public async Task ScopeLine_With_Content_Should_Format_Class_Structure() _ = builder.Append("private int _field;"); } - var result = builder.ToString(); - _ = await Assert.That(result).Contains("public class MyClass"); - _ = await Assert.That(result).Contains("private int _field;"); + var result = builder.ToString().Replace("\r\n", "\n", StringComparison.Ordinal); + var expected = """ + public class MyClass + { + private int _field;} + + """.Replace("\r\n", "\n", StringComparison.Ordinal); + _ = await Assert.That(result).IsEqualTo(expected); } [Test] @@ -217,10 +275,17 @@ public async Task ScopeLine_Nested_Should_Create_Class_Hierarchy() } } - var result = builder.ToString(); - _ = await Assert.That(result).Contains("namespace MyNamespace"); - _ = await Assert.That(result).Contains("public class OuterClass"); - _ = await Assert.That(result).Contains("public void Method() { }"); + var result = builder.ToString().Replace("\r\n", "\n", StringComparison.Ordinal); + var expected = """ + namespace MyNamespace + { + public class OuterClass + { + public void Method() { }} + } + + """.Replace("\r\n", "\n", StringComparison.Ordinal); + _ = await Assert.That(result).IsEqualTo(expected); } [Test] @@ -233,8 +298,14 @@ public async Task ScopeLine_Should_Apply_Proper_Indentation_To_Content() _ = builder.Append("public string Name { get; set; }"); } - var result = builder.ToString(); - _ = await Assert.That(result).Contains(" public string Name { get; set; }"); + var result = builder.ToString().Replace("\r\n", "\n", StringComparison.Ordinal); + var expected = """ + public class MyClass + { + public string Name { get; set; }} + + """.Replace("\r\n", "\n", StringComparison.Ordinal); + _ = await Assert.That(result).IsEqualTo(expected); } [Test] @@ -254,9 +325,18 @@ public async Task ScopeLine_Multiple_Sequential_Should_Create_Multiple_Classes() _ = builder.Append("public string Name { get; set; }"); } - var result = builder.ToString(); - _ = await Assert.That(result).Contains("public class FirstClass"); - _ = await Assert.That(result).Contains("public class SecondClass"); + var result = builder.ToString().Replace("\r\n", "\n", StringComparison.Ordinal); + var expected = """ + public class FirstClass + { + public int Id { get; set; }} + + public class SecondClass + { + public string Name { get; set; }} + + """.Replace("\r\n", "\n", StringComparison.Ordinal); + _ = await Assert.That(result).IsEqualTo(expected); } [Test] @@ -270,7 +350,16 @@ public async Task ScopeLine_With_Tabs_Should_Use_Tab_Indentation() } var result = builder.ToString(); - _ = await Assert.That(result).Contains("\tpublic void Method() { }"); + _ = await Assert + .That(result) + .IsEqualTo( + "public class MyClass" + + Environment.NewLine + + "{" + + Environment.NewLine + + "\tpublic void Method() { }}" + + Environment.NewLine + ); } [Test] @@ -289,11 +378,20 @@ public async Task ScopeLine_Complex_Nested_Structure_Should_Format_Correctly() } } - var result = builder.ToString(); - _ = await Assert.That(result).Contains("namespace MyApplication"); - _ = await Assert.That(result).Contains(" public class MyClass"); - _ = await Assert.That(result).Contains(" public void MyMethod()"); - _ = await Assert.That(result).Contains(" var x = 10;"); + var result = builder.ToString().Replace("\r\n", "\n", StringComparison.Ordinal); + var expected = """ + namespace MyApplication + { + public class MyClass + { + public void MyMethod() + { + var x = 10;} + } + } + + """.Replace("\r\n", "\n", StringComparison.Ordinal); + _ = await Assert.That(result).IsEqualTo(expected); } [Test] @@ -319,11 +417,20 @@ public async Task ScopeLine_With_Documentation_Should_Format_Complete_Class() _ = builder.Append("public string Name { get; set; }"); } - var result = builder.ToString(); - _ = await Assert.That(result).Contains("/// "); - _ = await Assert.That(result).Contains("/// Represents a person entity."); - _ = await Assert.That(result).Contains("public class Person"); - _ = await Assert.That(result).Contains("public string Name { get; set; }"); + var result = builder.ToString().Replace("\r\n", "\n", StringComparison.Ordinal); + var expected = """ + /// + /// Represents a person entity. + /// + public class Person + { + /// + /// Gets or sets the person's name. + /// + public string Name { get; set; }} + + """.Replace("\r\n", "\n", StringComparison.Ordinal); + _ = await Assert.That(result).IsEqualTo(expected); } #endregion From 62f30942c04302cb39c4bd6f75b23709db10a684 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Thu, 9 Apr 2026 15:28:14 +0200 Subject: [PATCH 2/4] fix: Update src/NetEvolve.CodeBuilder/CSharpCodeBuilder.Append.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: Martin Stühmer --- .../CSharpCodeBuilder.Append.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.Append.cs b/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.Append.cs index 8f3cae5..ee2ed53 100644 --- a/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.Append.cs +++ b/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.Append.cs @@ -206,19 +206,21 @@ public CSharpCodeBuilder Append(ReadOnlySpan value) /// If the span is empty, the method returns without appending anything. public CSharpCodeBuilder Append(ReadOnlySpan value, int startIndex, int count) { - if (value.IsEmpty) + var slice = value.Slice(startIndex, count); + if (slice.IsEmpty) { return this; } EnsureIndented(); -#if NETSTANDARD2_0 - _ = _builder.Append(value.Slice(startIndex, count).ToString()); -#else - _ = _builder.Append(value.Slice(startIndex, count)); -#endif +`#if` NETSTANDARD2_0 + _ = _builder.Append(slice.ToString()); +`#else` + _ = _builder.Append(slice); +`#endif` return this; } + } /// /// Appends a subset of a string to the current builder. From e039b7fdf03b0e2a60c9708e9e4f2d6baae8aa83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Thu, 9 Apr 2026 22:04:45 +0200 Subject: [PATCH 3/4] feat: Add interpolated string support and FormattableString overloads Added AppendInterpolated/AppendLineInterpolated methods with a custom interpolated string handler for efficient, culture-invariant code generation (C# 10+). Introduced FormattableString overloads for AppendFormat and AppendLineFormat. Added comprehensive unit tests for new features. Improved code formatting and performed minor test cleanup. --- .../CSharpCodeBuilder.Append.cs | 7 +- .../CSharpCodeBuilder.AppendFormat.cs | 66 +++++- .../CSharpCodeBuilder.AppendInterpolated.cs | 194 ++++++++++++++++++ .../CSharpCodeBuilder.Clear.cs | 2 +- .../CSharpCodeBuilderTests.AppendFormat.cs | 59 +++++- ...harpCodeBuilderTests.AppendInterpolated.cs | 132 ++++++++++++ ...CSharpCodeBuilderTests.AppendLineFormat.cs | 155 ++++++++++++++ .../CSharpCodeBuilderTests.Indentation.cs | 2 - 8 files changed, 608 insertions(+), 9 deletions(-) create mode 100644 src/NetEvolve.CodeBuilder/CSharpCodeBuilder.AppendInterpolated.cs create mode 100644 tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.AppendInterpolated.cs create mode 100644 tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.AppendLineFormat.cs diff --git a/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.Append.cs b/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.Append.cs index ee2ed53..7b91668 100644 --- a/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.Append.cs +++ b/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.Append.cs @@ -213,14 +213,13 @@ public CSharpCodeBuilder Append(ReadOnlySpan value, int startIndex, int co } EnsureIndented(); -`#if` NETSTANDARD2_0 +#if NETSTANDARD2_0 _ = _builder.Append(slice.ToString()); -`#else` +#else _ = _builder.Append(slice); -`#endif` +#endif return this; } - } /// /// Appends a subset of a string to the current builder. diff --git a/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.AppendFormat.cs b/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.AppendFormat.cs index 8b7b608..1d2245f 100644 --- a/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.AppendFormat.cs +++ b/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.AppendFormat.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.CodeBuilder; +namespace NetEvolve.CodeBuilder; using System; using System.Globalization; @@ -45,4 +45,68 @@ public CSharpCodeBuilder AppendFormat(IFormatProvider? provider, string format, _ = _builder.AppendFormat(provider, format, args); return this; } + + /// + /// Appends a formattable string to the current builder using invariant culture. + /// + /// The formattable string to append. + /// The current instance to allow for method chaining. + /// If is , the method returns without appending anything. + public CSharpCodeBuilder AppendFormat(FormattableString? formattable) + { + if (formattable is null) + { + return this; + } + + EnsureIndented(); + _ = _builder.Append(formattable.ToString(CultureInfo.InvariantCulture)); + return this; + } + + /// + /// Appends a formatted string followed by a line terminator to the current builder using invariant culture. + /// + /// A composite format string. + /// The object to format. + /// The current instance to allow for method chaining. + /// Thrown when is . + /// Thrown when is invalid or the index of a format item is greater than zero. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public CSharpCodeBuilder AppendLineFormat(string format, object? arg0) => + AppendFormat(CultureInfo.InvariantCulture, format, arg0).AppendLine(); + + /// + /// Appends a formatted string followed by a line terminator to the current builder using invariant culture. + /// + /// A composite format string. + /// An array of objects to format. + /// The current instance to allow for method chaining. + /// Thrown when is . + /// Thrown when is invalid or the index of a format item is greater than the number of elements in minus 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public CSharpCodeBuilder AppendLineFormat(string format, params object?[] args) => + AppendFormat(CultureInfo.InvariantCulture, format, args).AppendLine(); + + /// + /// Appends a formatted string followed by a line terminator to the current builder using the specified format provider. + /// + /// An object that supplies culture-specific formatting information. + /// A composite format string. + /// An array of objects to format. + /// The current instance to allow for method chaining. + /// Thrown when is . + /// Thrown when is invalid or the index of a format item is greater than the number of elements in minus 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public CSharpCodeBuilder AppendLineFormat(IFormatProvider? provider, string format, params object?[] args) => + AppendFormat(provider, format, args).AppendLine(); + + /// + /// Appends a formattable string followed by a line terminator to the current builder using invariant culture. + /// + /// The formattable string to append. + /// The current instance to allow for method chaining. + /// If is , only the line terminator is appended. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public CSharpCodeBuilder AppendLineFormat(FormattableString? formattable) => AppendFormat(formattable).AppendLine(); } diff --git a/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.AppendInterpolated.cs b/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.AppendInterpolated.cs new file mode 100644 index 0000000..00ad74c --- /dev/null +++ b/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.AppendInterpolated.cs @@ -0,0 +1,194 @@ +namespace NetEvolve.CodeBuilder; + +#if NET6_0_OR_GREATER +using System.Globalization; +using System.Runtime.CompilerServices; + +public partial class CSharpCodeBuilder +{ + /// + /// Appends an interpolated string to the current builder. + /// + /// The interpolated string handler built by the compiler. + /// The current instance to allow for method chaining. + /// + /// + /// builder.AppendInterpolated($"public {typeName} {memberName}"); + /// + /// + public CSharpCodeBuilder AppendInterpolated( + [InterpolatedStringHandlerArgument("")] ref CSharpInterpolatedStringHandler handler + ) => this; + + /// + /// Appends an interpolated string followed by a line terminator to the current builder. + /// + /// The interpolated string handler built by the compiler. + /// The current instance to allow for method chaining. + /// + /// + /// builder.AppendLineInterpolated($"public class {className}"); + /// + /// + public CSharpCodeBuilder AppendLineInterpolated( + [InterpolatedStringHandlerArgument("")] ref CSharpInterpolatedStringHandler handler + ) => AppendLine(); + + internal void HandlerEnsureIndented() => EnsureIndented(); + + internal void HandlerRawAppend(string? value) + { + if (!string.IsNullOrEmpty(value)) + { + _ = _builder.Append(value); + } + } + + internal void HandlerRawAppend(ReadOnlySpan value) + { + if (!value.IsEmpty) + { + _ = _builder.Append(value); + } + } +} + +/// +/// Custom interpolated string handler for . +/// +/// +/// This handler is instantiated by the compiler when an interpolated string is passed to +/// or . +/// It appends each literal and formatted part directly to the builder, applying indentation before +/// the first non-empty part on a new line. +/// +[InterpolatedStringHandler] +public ref struct CSharpInterpolatedStringHandler +{ + private readonly CSharpCodeBuilder _owner; + private bool _indentEnsured; + + /// + /// Initializes a new instance of the struct. + /// + /// The total length of all literal parts (hint for capacity). + /// The number of formatted holes in the interpolated string. + /// The to append to. + public CSharpInterpolatedStringHandler(int literalLength, int formattedCount, CSharpCodeBuilder builder) + { + _owner = builder; + _indentEnsured = false; + } + + private void EnsureIndented() + { + if (!_indentEnsured) + { + _owner.HandlerEnsureIndented(); + _indentEnsured = true; + } + } + + /// Appends a literal string part of the interpolated string. + /// The literal string to append. + public void AppendLiteral(string? value) + { + if (string.IsNullOrEmpty(value)) + { + return; + } + + EnsureIndented(); + _owner.HandlerRawAppend(value); + } + + /// Appends a formatted value from the interpolated string. + /// The type of the value to format. + /// The value to append. + public void AppendFormatted(T value) + { + var str = value?.ToString(); + if (string.IsNullOrEmpty(str)) + { + return; + } + + EnsureIndented(); + _owner.HandlerRawAppend(str); + } + + /// Appends a formatted value with a format string from the interpolated string. + /// The type of the value to format. Must implement . + /// The value to append. + /// The format string. + public void AppendFormatted(T value, string? format) + where T : System.IFormattable + { + var str = value?.ToString(format, CultureInfo.InvariantCulture); + if (string.IsNullOrEmpty(str)) + { + return; + } + + EnsureIndented(); + _owner.HandlerRawAppend(str); + } + + /// Appends a formatted value with alignment from the interpolated string. + /// The type of the value to format. + /// The value to append. + /// Minimum width; negative values left-align. + public void AppendFormatted(T value, int alignment) + { + var str = value?.ToString(); + if (str is null) + { + return; + } + + str = alignment >= 0 ? str.PadLeft(alignment) : str.PadRight(-alignment); + EnsureIndented(); + _owner.HandlerRawAppend(str); + } + + /// Appends a formatted value with alignment and format string from the interpolated string. + /// The type of the value to format. Must implement . + /// The value to append. + /// Minimum width; negative values left-align. + /// The format string. + public void AppendFormatted(T value, int alignment, string? format) + where T : System.IFormattable + { + var str = value?.ToString(format, CultureInfo.InvariantCulture) ?? string.Empty; + str = alignment >= 0 ? str.PadLeft(alignment) : str.PadRight(-alignment); + EnsureIndented(); + _owner.HandlerRawAppend(str); + } + + /// Appends a string value from the interpolated string. + /// The string to append. + public void AppendFormatted(string? value) + { + if (string.IsNullOrEmpty(value)) + { + return; + } + + EnsureIndented(); + _owner.HandlerRawAppend(value); + } + + /// Appends a value from the interpolated string. + /// The span to append. + public void AppendFormatted(ReadOnlySpan value) + { + if (value.IsEmpty) + { + return; + } + + EnsureIndented(); + _owner.HandlerRawAppend(value); + } +} +#endif diff --git a/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.Clear.cs b/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.Clear.cs index 56bbe56..9be24a9 100644 --- a/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.Clear.cs +++ b/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.Clear.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.CodeBuilder; +namespace NetEvolve.CodeBuilder; public partial class CSharpCodeBuilder { diff --git a/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.AppendFormat.cs b/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.AppendFormat.cs index 7bd20d5..62c7e90 100644 --- a/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.AppendFormat.cs +++ b/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.AppendFormat.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.CodeBuilder.Tests.Unit; +namespace NetEvolve.CodeBuilder.Tests.Unit; using System; using System.Diagnostics.CodeAnalysis; @@ -603,4 +603,61 @@ public async Task AppendFormat_ParamsArgs_NoProvider_Single_Element_Array_Should } #pragma warning restore CA1305 + + [Test] + public async Task AppendFormat_FormattableString_Should_Format_Correctly() + { + var builder = new CSharpCodeBuilder(10); + var typeName = "string"; + var memberName = "Name"; + + _ = builder.AppendFormat((FormattableString)$"public {typeName} {memberName}"); + + _ = await Assert.That(builder.ToString()).IsEqualTo("public string Name"); + } + + [Test] + public async Task AppendFormat_FormattableString_Should_Apply_Indentation() + { + var builder = new CSharpCodeBuilder(10); + builder.IncrementIndent(); + var value = 42; + + _ = builder.AppendLine().AppendFormat((FormattableString)$"Value: {value}"); + + _ = await Assert.That(builder.ToString()).IsEqualTo(Environment.NewLine + " Value: 42"); + } + + [Test] + public async Task AppendFormat_FormattableString_Null_Should_Return_Same_Instance_Without_Appending() + { + var builder = new CSharpCodeBuilder(10); + + var result = builder.AppendFormat((FormattableString?)null); + + _ = await Assert.That(result).IsEqualTo(builder); + _ = await Assert.That(builder.ToString()).IsEqualTo(string.Empty); + } + + [Test] + public async Task AppendFormat_FormattableString_Should_Use_InvariantCulture() + { + var builder = new CSharpCodeBuilder(10); + var value = 1234.56m; + + _ = builder.AppendFormat((FormattableString)$"{value:N2}"); + + _ = await Assert.That(builder.ToString()).IsEqualTo("1,234.56"); + } + + [Test] + public async Task AppendFormat_FormattableString_Should_Return_Same_Instance() + { + var builder = new CSharpCodeBuilder(10); + var text = "Hello"; + + var result = builder.AppendFormat((FormattableString)$"{text}"); + + _ = await Assert.That(result).IsEqualTo(builder); + } } diff --git a/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.AppendInterpolated.cs b/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.AppendInterpolated.cs new file mode 100644 index 0000000..ad17f71 --- /dev/null +++ b/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.AppendInterpolated.cs @@ -0,0 +1,132 @@ +#if NET6_0_OR_GREATER +namespace NetEvolve.CodeBuilder.Tests.Unit; + +using System; + +public partial class CSharpCodeBuilderTests +{ + [Test] + public async Task AppendInterpolated_LiteralOnly_Should_Append_Correctly() + { + var builder = new CSharpCodeBuilder(); + + _ = builder.AppendInterpolated($"Hello World"); + + _ = await Assert.That(builder.ToString()).IsEqualTo("Hello World"); + } + + [Test] + public async Task AppendInterpolated_WithFormattedValue_Should_Append_Correctly() + { + var builder = new CSharpCodeBuilder(); + var typeName = "string"; + var memberName = "Name"; + + _ = builder.AppendInterpolated($"public {typeName} {memberName}"); + + _ = await Assert.That(builder.ToString()).IsEqualTo("public string Name"); + } + + [Test] + public async Task AppendInterpolated_Should_Apply_Indentation() + { + var builder = new CSharpCodeBuilder(); + builder.IncrementIndent(); + var value = 42; + + _ = builder.AppendLine().AppendInterpolated($"Value: {value}"); + + var expected = Environment.NewLine + " Value: 42"; + _ = await Assert.That(builder.ToString()).IsEqualTo(expected); + } + + [Test] + public async Task AppendInterpolated_Should_Return_Same_Instance() + { + var builder = new CSharpCodeBuilder(); + + var result = builder.AppendInterpolated($"Hello"); + + _ = await Assert.That(result).IsEqualTo(builder); + } + + [Test] + public async Task AppendInterpolated_Empty_Should_Not_Append_Indentation() + { + var builder = new CSharpCodeBuilder(); + builder.IncrementIndent(); + + _ = builder.AppendLine().AppendInterpolated($""); + + _ = await Assert.That(builder.ToString()).IsEqualTo(Environment.NewLine); + } + + [Test] + public async Task AppendLineInterpolated_Should_Append_Line_Terminator() + { + var builder = new CSharpCodeBuilder(); + var typeName = "MyClass"; + + _ = builder.AppendLineInterpolated($"public class {typeName}"); + + _ = await Assert.That(builder.ToString()).IsEqualTo("public class MyClass" + Environment.NewLine); + } + + [Test] + public async Task AppendLineInterpolated_Should_Apply_Indentation() + { + var builder = new CSharpCodeBuilder(); + builder.IncrementIndent(); + var value = 1; + + _ = builder.AppendLine().AppendLineInterpolated($"Value: {value}"); + + var expected = Environment.NewLine + " Value: 1" + Environment.NewLine; + _ = await Assert.That(builder.ToString()).IsEqualTo(expected); + } + + [Test] + public async Task AppendLineInterpolated_Should_Return_Same_Instance() + { + var builder = new CSharpCodeBuilder(); + + var result = builder.AppendLineInterpolated($"Hello"); + + _ = await Assert.That(result).IsEqualTo(builder); + } + + [Test] + public async Task AppendInterpolated_FormattedValue_With_Format_Should_Use_InvariantCulture() + { + var builder = new CSharpCodeBuilder(); + var value = 1234.56m; + + _ = builder.AppendInterpolated($"{value:N2}"); + + _ = await Assert.That(builder.ToString()).IsEqualTo("1,234.56"); + } + + [Test] + public async Task AppendInterpolated_FormattedValue_With_Alignment_Should_PadLeft() + { + var builder = new CSharpCodeBuilder(); + var value = 42; + + _ = builder.AppendInterpolated($"{value, 6}"); + + _ = await Assert.That(builder.ToString()).IsEqualTo(" 42"); + } + + [Test] + public async Task AppendLineInterpolated_Consecutive_Should_Indent_Each_Line() + { + var builder = new CSharpCodeBuilder(); + builder.IncrementIndent(); + + _ = builder.AppendLine().AppendLineInterpolated($"Line 1").AppendLineInterpolated($"Line 2"); + + var expected = Environment.NewLine + " Line 1" + Environment.NewLine + " Line 2" + Environment.NewLine; + _ = await Assert.That(builder.ToString()).IsEqualTo(expected); + } +} +#endif diff --git a/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.AppendLineFormat.cs b/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.AppendLineFormat.cs new file mode 100644 index 0000000..1bae044 --- /dev/null +++ b/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.AppendLineFormat.cs @@ -0,0 +1,155 @@ +namespace NetEvolve.CodeBuilder.Tests.Unit; + +using System; +using System.Globalization; + +public partial class CSharpCodeBuilderTests +{ + [Test] + public async Task AppendLineFormat_OneArgument_Should_Format_Correctly() + { + var builder = new CSharpCodeBuilder(10); + + _ = builder.AppendLineFormat(CultureInfo.InvariantCulture, "Value: {0}", 42); + + _ = await Assert.That(builder.ToString()).IsEqualTo("Value: 42" + Environment.NewLine); + } + + [Test] + public async Task AppendLineFormat_OneArgument_Should_Use_InvariantCulture() + { + var builder = new CSharpCodeBuilder(10); + + _ = builder.AppendLineFormat("Value: {0}", (object?)42); + + _ = await Assert.That(builder.ToString()).IsEqualTo("Value: 42" + Environment.NewLine); + } + + // CA1305 is intentionally suppressed: the purpose of this test is to verify that the + // no-provider overload resolves correctly and delegates to InvariantCulture. +#pragma warning disable CA1305 + [Test] + public async Task AppendLineFormat_MultipleArguments_Should_Format_Correctly() + { + var builder = new CSharpCodeBuilder(10); + + _ = builder.AppendLineFormat("public {0} {1}", "string", "Name"); + + _ = await Assert.That(builder.ToString()).IsEqualTo("public string Name" + Environment.NewLine); + } +#pragma warning restore CA1305 + + [Test] + public async Task AppendLineFormat_Should_Apply_Indentation() + { + var builder = new CSharpCodeBuilder(10); + builder.IncrementIndent(); + + _ = builder.AppendLine().AppendLineFormat("Value: {0}", (object?)42); + + var expected = Environment.NewLine + " Value: 42" + Environment.NewLine; + _ = await Assert.That(builder.ToString()).IsEqualTo(expected); + } + + [Test] + public async Task AppendLineFormat_Should_Return_Same_Instance() + { + var builder = new CSharpCodeBuilder(10); + + var result = builder.AppendLineFormat("Value: {0}", (object?)42); + + _ = await Assert.That(result).IsEqualTo(builder); + } + + [Test] + public async Task AppendLineFormat_WithProvider_Should_Format_Correctly() + { + var builder = new CSharpCodeBuilder(10); + + _ = builder.AppendLineFormat(CultureInfo.InvariantCulture, "{0:N2}", 1234.56m); + + _ = await Assert.That(builder.ToString()).IsEqualTo("1,234.56" + Environment.NewLine); + } + + [Test] + public async Task AppendLineFormat_NullFormat_Should_Throw_ArgumentNullException() + { + var exception = Assert.Throws(() => + { + var builder = new CSharpCodeBuilder(10); + _ = builder.AppendLineFormat(CultureInfo.InvariantCulture, null!, 42); + }); + + _ = await Assert.That(exception).IsNotNull(); + } + + [Test] + public async Task AppendLineFormat_Should_Set_IsNewline_True_After_Call() + { + var builder = new CSharpCodeBuilder(10); + builder.IncrementIndent(); + + _ = builder.AppendLineFormat("Value: {0}", (object?)1).AppendLineFormat("Value: {0}", (object?)2); + + // Both lines are indented: _isNewline=true at builder start, so both calls get indentation applied. + var expected = " Value: 1" + Environment.NewLine + " Value: 2" + Environment.NewLine; + _ = await Assert.That(builder.ToString()).IsEqualTo(expected); + } + + [Test] + public async Task AppendLineFormat_FormattableString_Should_Format_Correctly() + { + var builder = new CSharpCodeBuilder(10); + var typeName = "string"; + var memberName = "Name"; + + _ = builder.AppendLineFormat((FormattableString)$"public {typeName} {memberName}"); + + _ = await Assert.That(builder.ToString()).IsEqualTo("public string Name" + Environment.NewLine); + } + + [Test] + public async Task AppendLineFormat_FormattableString_Should_Apply_Indentation() + { + var builder = new CSharpCodeBuilder(10); + builder.IncrementIndent(); + var value = 42; + + _ = builder.AppendLine().AppendLineFormat((FormattableString)$"Value: {value}"); + + var expected = Environment.NewLine + " Value: 42" + Environment.NewLine; + _ = await Assert.That(builder.ToString()).IsEqualTo(expected); + } + + [Test] + public async Task AppendLineFormat_FormattableString_Null_Should_Append_Only_LineTerminator() + { + var builder = new CSharpCodeBuilder(10); + + _ = builder.AppendLineFormat((FormattableString?)null); + + _ = await Assert.That(builder.ToString()).IsEqualTo(Environment.NewLine); + } + + [Test] + public async Task AppendLineFormat_FormattableString_Should_Use_InvariantCulture() + { + var builder = new CSharpCodeBuilder(10); + var value = 1234.56m; + + _ = builder.AppendLineFormat((FormattableString)$"{value:N2}"); + + _ = await Assert.That(builder.ToString()).IsEqualTo("1,234.56" + Environment.NewLine); + } + + [Test] + public async Task AppendLineFormat_FormattableString_Should_Return_Same_Instance() + { + var builder = new CSharpCodeBuilder(10); + var text = "Hello"; + + var result = builder.AppendLineFormat((FormattableString)$"{text}"); + + _ = await Assert.That(result).IsEqualTo(builder); + } +} diff --git a/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.Indentation.cs b/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.Indentation.cs index bcafd26..087923a 100644 --- a/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.Indentation.cs +++ b/tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.Indentation.cs @@ -139,8 +139,6 @@ public async Task DecrementIndent_Should_Not_Affect_Existing_Content() public async Task IncrementIndent_And_DecrementIndent_Multiple_Operations_Should_Work() { var builder = new CSharpCodeBuilder(); - - // Simulate nested code blocks - the braces automatically handle indentation _ = builder.Append("class MyClass"); _ = builder.Append("{"); // This automatically increments indent and adds newline _ = builder.Append("public void Method()"); From 43f7eb1f8b24edb31971fdacbdc46e1a2118845e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Thu, 9 Apr 2026 22:08:22 +0200 Subject: [PATCH 4/4] fix: Remove AppendFormatted(string?) from interpolated handler Removed the AppendFormatted(string?) method from CSharpInterpolatedStringHandler, as its functionality is now redundant. Also updated the #if NET6_0_OR_GREATER directive to wrap the namespace declaration for improved clarity and consistency. --- .../CSharpCodeBuilder.AppendInterpolated.cs | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.AppendInterpolated.cs b/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.AppendInterpolated.cs index 00ad74c..a562af4 100644 --- a/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.AppendInterpolated.cs +++ b/src/NetEvolve.CodeBuilder/CSharpCodeBuilder.AppendInterpolated.cs @@ -1,6 +1,6 @@ +#if NET6_0_OR_GREATER namespace NetEvolve.CodeBuilder; -#if NET6_0_OR_GREATER using System.Globalization; using System.Runtime.CompilerServices; @@ -165,19 +165,6 @@ public void AppendFormatted(T value, int alignment, string? format) _owner.HandlerRawAppend(str); } - /// Appends a string value from the interpolated string. - /// The string to append. - public void AppendFormatted(string? value) - { - if (string.IsNullOrEmpty(value)) - { - return; - } - - EnsureIndented(); - _owner.HandlerRawAppend(value); - } - /// Appends a value from the interpolated string. /// The span to append. public void AppendFormatted(ReadOnlySpan value)