Skip to content

Commit e039b7f

Browse files
committed
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.
1 parent 62f3094 commit e039b7f

8 files changed

Lines changed: 608 additions & 9 deletions

src/NetEvolve.CodeBuilder/CSharpCodeBuilder.Append.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -213,14 +213,13 @@ public CSharpCodeBuilder Append(ReadOnlySpan<char> value, int startIndex, int co
213213
}
214214

215215
EnsureIndented();
216-
`#if` NETSTANDARD2_0
216+
#if NETSTANDARD2_0
217217
_ = _builder.Append(slice.ToString());
218-
`#else`
218+
#else
219219
_ = _builder.Append(slice);
220-
`#endif`
220+
#endif
221221
return this;
222222
}
223-
}
224223

225224
/// <summary>
226225
/// Appends a subset of a string to the current builder.

src/NetEvolve.CodeBuilder/CSharpCodeBuilder.AppendFormat.cs

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
namespace NetEvolve.CodeBuilder;
1+
namespace NetEvolve.CodeBuilder;
22

33
using System;
44
using System.Globalization;
@@ -45,4 +45,68 @@ public CSharpCodeBuilder AppendFormat(IFormatProvider? provider, string format,
4545
_ = _builder.AppendFormat(provider, format, args);
4646
return this;
4747
}
48+
49+
/// <summary>
50+
/// Appends a formattable string to the current builder using invariant culture.
51+
/// </summary>
52+
/// <param name="formattable">The formattable string to append.</param>
53+
/// <returns>The current <see cref="CSharpCodeBuilder"/> instance to allow for method chaining.</returns>
54+
/// <remarks>If <paramref name="formattable"/> is <see langword="null"/>, the method returns without appending anything.</remarks>
55+
public CSharpCodeBuilder AppendFormat(FormattableString? formattable)
56+
{
57+
if (formattable is null)
58+
{
59+
return this;
60+
}
61+
62+
EnsureIndented();
63+
_ = _builder.Append(formattable.ToString(CultureInfo.InvariantCulture));
64+
return this;
65+
}
66+
67+
/// <summary>
68+
/// Appends a formatted string followed by a line terminator to the current builder using invariant culture.
69+
/// </summary>
70+
/// <param name="format">A composite format string.</param>
71+
/// <param name="arg0">The object to format.</param>
72+
/// <returns>The current <see cref="CSharpCodeBuilder"/> instance to allow for method chaining.</returns>
73+
/// <exception cref="ArgumentNullException">Thrown when <paramref name="format"/> is <see langword="null"/>.</exception>
74+
/// <exception cref="FormatException">Thrown when <paramref name="format"/> is invalid or the index of a format item is greater than zero.</exception>
75+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
76+
public CSharpCodeBuilder AppendLineFormat(string format, object? arg0) =>
77+
AppendFormat(CultureInfo.InvariantCulture, format, arg0).AppendLine();
78+
79+
/// <summary>
80+
/// Appends a formatted string followed by a line terminator to the current builder using invariant culture.
81+
/// </summary>
82+
/// <param name="format">A composite format string.</param>
83+
/// <param name="args">An array of objects to format.</param>
84+
/// <returns>The current <see cref="CSharpCodeBuilder"/> instance to allow for method chaining.</returns>
85+
/// <exception cref="ArgumentNullException">Thrown when <paramref name="format"/> is <see langword="null"/>.</exception>
86+
/// <exception cref="FormatException">Thrown when <paramref name="format"/> is invalid or the index of a format item is greater than the number of elements in <paramref name="args"/> minus 1.</exception>
87+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
88+
public CSharpCodeBuilder AppendLineFormat(string format, params object?[] args) =>
89+
AppendFormat(CultureInfo.InvariantCulture, format, args).AppendLine();
90+
91+
/// <summary>
92+
/// Appends a formatted string followed by a line terminator to the current builder using the specified format provider.
93+
/// </summary>
94+
/// <param name="provider">An object that supplies culture-specific formatting information.</param>
95+
/// <param name="format">A composite format string.</param>
96+
/// <param name="args">An array of objects to format.</param>
97+
/// <returns>The current <see cref="CSharpCodeBuilder"/> instance to allow for method chaining.</returns>
98+
/// <exception cref="ArgumentNullException">Thrown when <paramref name="format"/> is <see langword="null"/>.</exception>
99+
/// <exception cref="FormatException">Thrown when <paramref name="format"/> is invalid or the index of a format item is greater than the number of elements in <paramref name="args"/> minus 1.</exception>
100+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
101+
public CSharpCodeBuilder AppendLineFormat(IFormatProvider? provider, string format, params object?[] args) =>
102+
AppendFormat(provider, format, args).AppendLine();
103+
104+
/// <summary>
105+
/// Appends a formattable string followed by a line terminator to the current builder using invariant culture.
106+
/// </summary>
107+
/// <param name="formattable">The formattable string to append.</param>
108+
/// <returns>The current <see cref="CSharpCodeBuilder"/> instance to allow for method chaining.</returns>
109+
/// <remarks>If <paramref name="formattable"/> is <see langword="null"/>, only the line terminator is appended.</remarks>
110+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
111+
public CSharpCodeBuilder AppendLineFormat(FormattableString? formattable) => AppendFormat(formattable).AppendLine();
48112
}
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
namespace NetEvolve.CodeBuilder;
2+
3+
#if NET6_0_OR_GREATER
4+
using System.Globalization;
5+
using System.Runtime.CompilerServices;
6+
7+
public partial class CSharpCodeBuilder
8+
{
9+
/// <summary>
10+
/// Appends an interpolated string to the current builder.
11+
/// </summary>
12+
/// <param name="handler">The interpolated string handler built by the compiler.</param>
13+
/// <returns>The current <see cref="CSharpCodeBuilder"/> instance to allow for method chaining.</returns>
14+
/// <example>
15+
/// <code>
16+
/// builder.AppendInterpolated($"public {typeName} {memberName}");
17+
/// </code>
18+
/// </example>
19+
public CSharpCodeBuilder AppendInterpolated(
20+
[InterpolatedStringHandlerArgument("")] ref CSharpInterpolatedStringHandler handler
21+
) => this;
22+
23+
/// <summary>
24+
/// Appends an interpolated string followed by a line terminator to the current builder.
25+
/// </summary>
26+
/// <param name="handler">The interpolated string handler built by the compiler.</param>
27+
/// <returns>The current <see cref="CSharpCodeBuilder"/> instance to allow for method chaining.</returns>
28+
/// <example>
29+
/// <code>
30+
/// builder.AppendLineInterpolated($"public class {className}");
31+
/// </code>
32+
/// </example>
33+
public CSharpCodeBuilder AppendLineInterpolated(
34+
[InterpolatedStringHandlerArgument("")] ref CSharpInterpolatedStringHandler handler
35+
) => AppendLine();
36+
37+
internal void HandlerEnsureIndented() => EnsureIndented();
38+
39+
internal void HandlerRawAppend(string? value)
40+
{
41+
if (!string.IsNullOrEmpty(value))
42+
{
43+
_ = _builder.Append(value);
44+
}
45+
}
46+
47+
internal void HandlerRawAppend(ReadOnlySpan<char> value)
48+
{
49+
if (!value.IsEmpty)
50+
{
51+
_ = _builder.Append(value);
52+
}
53+
}
54+
}
55+
56+
/// <summary>
57+
/// Custom interpolated string handler for <see cref="CSharpCodeBuilder"/>.
58+
/// </summary>
59+
/// <remarks>
60+
/// This handler is instantiated by the compiler when an interpolated string is passed to
61+
/// <see cref="CSharpCodeBuilder.AppendInterpolated"/> or <see cref="CSharpCodeBuilder.AppendLineInterpolated"/>.
62+
/// It appends each literal and formatted part directly to the builder, applying indentation before
63+
/// the first non-empty part on a new line.
64+
/// </remarks>
65+
[InterpolatedStringHandler]
66+
public ref struct CSharpInterpolatedStringHandler
67+
{
68+
private readonly CSharpCodeBuilder _owner;
69+
private bool _indentEnsured;
70+
71+
/// <summary>
72+
/// Initializes a new instance of the <see cref="CSharpInterpolatedStringHandler"/> struct.
73+
/// </summary>
74+
/// <param name="literalLength">The total length of all literal parts (hint for capacity).</param>
75+
/// <param name="formattedCount">The number of formatted holes in the interpolated string.</param>
76+
/// <param name="builder">The <see cref="CSharpCodeBuilder"/> to append to.</param>
77+
public CSharpInterpolatedStringHandler(int literalLength, int formattedCount, CSharpCodeBuilder builder)
78+
{
79+
_owner = builder;
80+
_indentEnsured = false;
81+
}
82+
83+
private void EnsureIndented()
84+
{
85+
if (!_indentEnsured)
86+
{
87+
_owner.HandlerEnsureIndented();
88+
_indentEnsured = true;
89+
}
90+
}
91+
92+
/// <summary>Appends a literal string part of the interpolated string.</summary>
93+
/// <param name="value">The literal string to append.</param>
94+
public void AppendLiteral(string? value)
95+
{
96+
if (string.IsNullOrEmpty(value))
97+
{
98+
return;
99+
}
100+
101+
EnsureIndented();
102+
_owner.HandlerRawAppend(value);
103+
}
104+
105+
/// <summary>Appends a formatted value from the interpolated string.</summary>
106+
/// <typeparam name="T">The type of the value to format.</typeparam>
107+
/// <param name="value">The value to append.</param>
108+
public void AppendFormatted<T>(T value)
109+
{
110+
var str = value?.ToString();
111+
if (string.IsNullOrEmpty(str))
112+
{
113+
return;
114+
}
115+
116+
EnsureIndented();
117+
_owner.HandlerRawAppend(str);
118+
}
119+
120+
/// <summary>Appends a formatted value with a format string from the interpolated string.</summary>
121+
/// <typeparam name="T">The type of the value to format. Must implement <see cref="System.IFormattable"/>.</typeparam>
122+
/// <param name="value">The value to append.</param>
123+
/// <param name="format">The format string.</param>
124+
public void AppendFormatted<T>(T value, string? format)
125+
where T : System.IFormattable
126+
{
127+
var str = value?.ToString(format, CultureInfo.InvariantCulture);
128+
if (string.IsNullOrEmpty(str))
129+
{
130+
return;
131+
}
132+
133+
EnsureIndented();
134+
_owner.HandlerRawAppend(str);
135+
}
136+
137+
/// <summary>Appends a formatted value with alignment from the interpolated string.</summary>
138+
/// <typeparam name="T">The type of the value to format.</typeparam>
139+
/// <param name="value">The value to append.</param>
140+
/// <param name="alignment">Minimum width; negative values left-align.</param>
141+
public void AppendFormatted<T>(T value, int alignment)
142+
{
143+
var str = value?.ToString();
144+
if (str is null)
145+
{
146+
return;
147+
}
148+
149+
str = alignment >= 0 ? str.PadLeft(alignment) : str.PadRight(-alignment);
150+
EnsureIndented();
151+
_owner.HandlerRawAppend(str);
152+
}
153+
154+
/// <summary>Appends a formatted value with alignment and format string from the interpolated string.</summary>
155+
/// <typeparam name="T">The type of the value to format. Must implement <see cref="System.IFormattable"/>.</typeparam>
156+
/// <param name="value">The value to append.</param>
157+
/// <param name="alignment">Minimum width; negative values left-align.</param>
158+
/// <param name="format">The format string.</param>
159+
public void AppendFormatted<T>(T value, int alignment, string? format)
160+
where T : System.IFormattable
161+
{
162+
var str = value?.ToString(format, CultureInfo.InvariantCulture) ?? string.Empty;
163+
str = alignment >= 0 ? str.PadLeft(alignment) : str.PadRight(-alignment);
164+
EnsureIndented();
165+
_owner.HandlerRawAppend(str);
166+
}
167+
168+
/// <summary>Appends a string value from the interpolated string.</summary>
169+
/// <param name="value">The string to append.</param>
170+
public void AppendFormatted(string? value)
171+
{
172+
if (string.IsNullOrEmpty(value))
173+
{
174+
return;
175+
}
176+
177+
EnsureIndented();
178+
_owner.HandlerRawAppend(value);
179+
}
180+
181+
/// <summary>Appends a <see cref="ReadOnlySpan{T}"/> value from the interpolated string.</summary>
182+
/// <param name="value">The span to append.</param>
183+
public void AppendFormatted(ReadOnlySpan<char> value)
184+
{
185+
if (value.IsEmpty)
186+
{
187+
return;
188+
}
189+
190+
EnsureIndented();
191+
_owner.HandlerRawAppend(value);
192+
}
193+
}
194+
#endif

src/NetEvolve.CodeBuilder/CSharpCodeBuilder.Clear.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
namespace NetEvolve.CodeBuilder;
1+
namespace NetEvolve.CodeBuilder;
22

33
public partial class CSharpCodeBuilder
44
{

tests/NetEvolve.CodeBuilder.Tests.Unit/CSharpCodeBuilderTests.AppendFormat.cs

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
namespace NetEvolve.CodeBuilder.Tests.Unit;
1+
namespace NetEvolve.CodeBuilder.Tests.Unit;
22

33
using System;
44
using System.Diagnostics.CodeAnalysis;
@@ -603,4 +603,61 @@ public async Task AppendFormat_ParamsArgs_NoProvider_Single_Element_Array_Should
603603
}
604604

605605
#pragma warning restore CA1305
606+
607+
[Test]
608+
public async Task AppendFormat_FormattableString_Should_Format_Correctly()
609+
{
610+
var builder = new CSharpCodeBuilder(10);
611+
var typeName = "string";
612+
var memberName = "Name";
613+
614+
_ = builder.AppendFormat((FormattableString)$"public {typeName} {memberName}");
615+
616+
_ = await Assert.That(builder.ToString()).IsEqualTo("public string Name");
617+
}
618+
619+
[Test]
620+
public async Task AppendFormat_FormattableString_Should_Apply_Indentation()
621+
{
622+
var builder = new CSharpCodeBuilder(10);
623+
builder.IncrementIndent();
624+
var value = 42;
625+
626+
_ = builder.AppendLine().AppendFormat((FormattableString)$"Value: {value}");
627+
628+
_ = await Assert.That(builder.ToString()).IsEqualTo(Environment.NewLine + " Value: 42");
629+
}
630+
631+
[Test]
632+
public async Task AppendFormat_FormattableString_Null_Should_Return_Same_Instance_Without_Appending()
633+
{
634+
var builder = new CSharpCodeBuilder(10);
635+
636+
var result = builder.AppendFormat((FormattableString?)null);
637+
638+
_ = await Assert.That(result).IsEqualTo(builder);
639+
_ = await Assert.That(builder.ToString()).IsEqualTo(string.Empty);
640+
}
641+
642+
[Test]
643+
public async Task AppendFormat_FormattableString_Should_Use_InvariantCulture()
644+
{
645+
var builder = new CSharpCodeBuilder(10);
646+
var value = 1234.56m;
647+
648+
_ = builder.AppendFormat((FormattableString)$"{value:N2}");
649+
650+
_ = await Assert.That(builder.ToString()).IsEqualTo("1,234.56");
651+
}
652+
653+
[Test]
654+
public async Task AppendFormat_FormattableString_Should_Return_Same_Instance()
655+
{
656+
var builder = new CSharpCodeBuilder(10);
657+
var text = "Hello";
658+
659+
var result = builder.AppendFormat((FormattableString)$"{text}");
660+
661+
_ = await Assert.That(result).IsEqualTo(builder);
662+
}
606663
}

0 commit comments

Comments
 (0)