Skip to content

Commit cafbee4

Browse files
Copilotsamtrion
andcommitted
chore: Implement Scope method for automatic indentation management with comprehensive tests
Co-authored-by: samtrion <[email protected]>
1 parent a154256 commit cafbee4

File tree

3 files changed

+369
-1
lines changed

3 files changed

+369
-1
lines changed
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
namespace NetEvolve.CodeBuilder;
2+
3+
using System;
4+
5+
/// <summary>
6+
/// A disposable struct that manages indentation scope for a <see cref="CSharpCodeBuilder"/>.
7+
/// </summary>
8+
/// <remarks>
9+
/// This struct increments the indentation level when created and decrements it when disposed.
10+
/// It is designed to work with the 'using' statement to provide automatic indentation management.
11+
/// </remarks>
12+
public readonly struct ScopeHandler : IDisposable, IEquatable<ScopeHandler>
13+
{
14+
private readonly CSharpCodeBuilder _builder;
15+
16+
/// <summary>
17+
/// Initializes a new instance of the <see cref="ScopeHandler"/> struct and increments the indentation level.
18+
/// </summary>
19+
/// <param name="builder">The <see cref="CSharpCodeBuilder"/> instance to manage indentation for.</param>
20+
internal ScopeHandler(CSharpCodeBuilder builder)
21+
{
22+
_builder = builder;
23+
_builder.IncrementIndent();
24+
}
25+
26+
/// <summary>
27+
/// Decrements the indentation level when the scope is disposed.
28+
/// </summary>
29+
/// <remarks>
30+
/// This method is called automatically when the 'using' statement block ends.
31+
/// </remarks>
32+
public void Dispose()
33+
{
34+
_builder?.DecrementIndent();
35+
}
36+
37+
/// <summary>
38+
/// Determines whether the specified object is equal to the current instance.
39+
/// </summary>
40+
/// <param name="obj">The object to compare with the current instance.</param>
41+
/// <returns>Always returns <c>false</c> since ScopeHandler instances should not be compared.</returns>
42+
public override readonly bool Equals(object? obj) => false;
43+
44+
/// <summary>
45+
/// Determines whether the specified ScopeHandler is equal to the current instance.
46+
/// </summary>
47+
/// <param name="other">The ScopeHandler to compare with the current instance.</param>
48+
/// <returns>Always returns <c>false</c> since ScopeHandler instances should not be compared.</returns>
49+
public readonly bool Equals(ScopeHandler other) => false;
50+
51+
/// <summary>
52+
/// Returns the hash code for this instance.
53+
/// </summary>
54+
/// <returns>A hash code based on the internal builder reference.</returns>
55+
public override readonly int GetHashCode() => _builder?.GetHashCode() ?? 0;
56+
57+
/// <summary>
58+
/// Determines whether two ScopeHandler instances are equal.
59+
/// </summary>
60+
/// <param name="left">The first instance to compare.</param>
61+
/// <param name="right">The second instance to compare.</param>
62+
/// <returns>Always returns <c>false</c> since ScopeHandler instances should not be compared.</returns>
63+
public static bool operator ==(ScopeHandler left, ScopeHandler right) => false;
64+
65+
/// <summary>
66+
/// Determines whether two ScopeHandler instances are not equal.
67+
/// </summary>
68+
/// <param name="left">The first instance to compare.</param>
69+
/// <param name="right">The second instance to compare.</param>
70+
/// <returns>Always returns <c>true</c> since ScopeHandler instances should not be compared.</returns>
71+
public static bool operator !=(ScopeHandler left, ScopeHandler right) => true;
72+
}
73+
74+
public partial record CSharpCodeBuilder
75+
{
76+
/// <summary>
77+
/// Creates a scope that automatically manages indentation levels.
78+
/// </summary>
79+
/// <returns>A <see cref="ScopeHandler"/> that increments indentation on creation and decrements on disposal.</returns>
80+
/// <remarks>
81+
/// The returned scope handler implements <see cref="IDisposable"/> and is designed for use with
82+
/// the 'using' statement. When the scope is created, indentation is incremented by one level.
83+
/// When the scope is disposed (at the end of the using block), indentation is decremented.
84+
/// </remarks>
85+
/// <example>
86+
/// <code>
87+
/// using (builder.Scope())
88+
/// {
89+
/// builder.AppendLine("return true;");
90+
/// }
91+
/// </code>
92+
/// </example>
93+
public ScopeHandler Scope() => new(this);
94+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ public async Task Append_CharArray_Empty_Should_Not_Change_Builder()
112112
{
113113
var builder = new CSharpCodeBuilder(10);
114114

115-
_ = builder.Append([]);
115+
_ = builder.Append((char[])[]);
116116

117117
_ = await Assert.That(builder.ToString()).IsEqualTo("");
118118
}
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
namespace NetEvolve.CodeBuilder.Tests.Unit;
2+
3+
using System;
4+
5+
public partial class CSharpCodeBuilderTests
6+
{
7+
[Test]
8+
public async Task Scope_Should_Increment_Indentation_On_Creation()
9+
{
10+
var builder = new CSharpCodeBuilder();
11+
12+
using (builder.Scope())
13+
{
14+
_ = builder.AppendLine().Append("test");
15+
}
16+
17+
var expected = Environment.NewLine + " test";
18+
_ = await Assert.That(builder.ToString()).IsEqualTo(expected);
19+
}
20+
21+
[Test]
22+
public async Task Scope_Should_Decrement_Indentation_On_Disposal()
23+
{
24+
var builder = new CSharpCodeBuilder();
25+
26+
using (builder.Scope())
27+
{
28+
_ = builder.AppendLine().Append("indented");
29+
}
30+
_ = builder.AppendLine().Append("normal");
31+
32+
var expected = Environment.NewLine + " indented" + Environment.NewLine + "normal";
33+
_ = await Assert.That(builder.ToString()).IsEqualTo(expected);
34+
}
35+
36+
[Test]
37+
public async Task Scope_Should_Support_Nested_Scopes()
38+
{
39+
var builder = new CSharpCodeBuilder();
40+
41+
using (builder.Scope())
42+
{
43+
_ = builder.AppendLine().Append("level 1");
44+
using (builder.Scope())
45+
{
46+
_ = builder.AppendLine().Append("level 2");
47+
using (builder.Scope())
48+
{
49+
_ = builder.AppendLine().Append("level 3");
50+
}
51+
_ = builder.AppendLine().Append("back to level 2");
52+
}
53+
_ = builder.AppendLine().Append("back to level 1");
54+
}
55+
_ = builder.AppendLine().Append("level 0");
56+
57+
var expected =
58+
Environment.NewLine
59+
+ " level 1"
60+
+ Environment.NewLine
61+
+ " level 2"
62+
+ Environment.NewLine
63+
+ " level 3"
64+
+ Environment.NewLine
65+
+ " back to level 2"
66+
+ Environment.NewLine
67+
+ " back to level 1"
68+
+ Environment.NewLine
69+
+ "level 0";
70+
71+
_ = await Assert.That(builder.ToString()).IsEqualTo(expected);
72+
}
73+
74+
[Test]
75+
public async Task Scope_Should_Work_With_Tabs()
76+
{
77+
var builder = new CSharpCodeBuilder { UseTabs = true };
78+
79+
using (builder.Scope())
80+
{
81+
_ = builder.AppendLine().Append("test");
82+
}
83+
84+
var expected = Environment.NewLine + "\ttest";
85+
_ = await Assert.That(builder.ToString()).IsEqualTo(expected);
86+
}
87+
88+
[Test]
89+
public async Task Scope_Should_Handle_Multiple_Sequential_Scopes()
90+
{
91+
var builder = new CSharpCodeBuilder();
92+
93+
using (builder.Scope())
94+
{
95+
_ = builder.AppendLine().Append("first scope");
96+
}
97+
98+
using (builder.Scope())
99+
{
100+
_ = builder.AppendLine().Append("second scope");
101+
}
102+
103+
_ = builder.AppendLine().Append("no scope");
104+
105+
var expected =
106+
Environment.NewLine
107+
+ " first scope"
108+
+ Environment.NewLine
109+
+ " second scope"
110+
+ Environment.NewLine
111+
+ "no scope";
112+
113+
_ = await Assert.That(builder.ToString()).IsEqualTo(expected);
114+
}
115+
116+
[Test]
117+
public async Task Scope_Should_Handle_Empty_Scope()
118+
{
119+
var builder = new CSharpCodeBuilder();
120+
121+
using (builder.Scope())
122+
{
123+
// Empty scope - should not affect builder content
124+
}
125+
126+
_ = builder.AppendLine().Append("test");
127+
128+
var expected = Environment.NewLine + "test";
129+
_ = await Assert.That(builder.ToString()).IsEqualTo(expected);
130+
}
131+
132+
[Test]
133+
public async Task Scope_Should_Handle_Exception_Within_Scope()
134+
{
135+
var builder = new CSharpCodeBuilder();
136+
137+
try
138+
{
139+
using (builder.Scope())
140+
{
141+
_ = builder.AppendLine().Append("before exception");
142+
throw new InvalidOperationException("Test exception");
143+
#pragma warning disable CS0162 // Unreachable code detected
144+
_ = builder.AppendLine().Append("after exception");
145+
#pragma warning restore CS0162 // Unreachable code detected
146+
}
147+
}
148+
catch (InvalidOperationException)
149+
{
150+
// Expected exception - indentation should still be properly decremented
151+
}
152+
153+
_ = builder.AppendLine().Append("after scope");
154+
155+
var expected = Environment.NewLine + " before exception" + Environment.NewLine + "after scope";
156+
_ = await Assert.That(builder.ToString()).IsEqualTo(expected);
157+
}
158+
159+
[Test]
160+
public async Task Scope_Should_Work_With_Existing_Indentation()
161+
{
162+
var builder = new CSharpCodeBuilder();
163+
164+
// Manually add some indentation first
165+
builder.IncrementIndent();
166+
_ = builder.AppendLine().Append("existing indent");
167+
168+
using (builder.Scope())
169+
{
170+
_ = builder.AppendLine().Append("scope indent");
171+
}
172+
173+
_ = builder.AppendLine().Append("back to existing");
174+
175+
var expected =
176+
Environment.NewLine
177+
+ " existing indent"
178+
+ Environment.NewLine
179+
+ " scope indent"
180+
+ Environment.NewLine
181+
+ " back to existing";
182+
183+
_ = await Assert.That(builder.ToString()).IsEqualTo(expected);
184+
}
185+
186+
[Test]
187+
public async Task Scope_Should_Work_With_Complex_Code_Generation()
188+
{
189+
var builder = new CSharpCodeBuilder();
190+
191+
_ = builder.AppendLine("public class TestClass");
192+
using (builder.Scope())
193+
{
194+
_ = builder.Append("public void TestMethod()").AppendLine();
195+
using (builder.Scope())
196+
{
197+
_ = builder.Append("if (true)").AppendLine();
198+
using (builder.Scope())
199+
{
200+
_ = builder.Append("Console.WriteLine(\"Hello, World!\");");
201+
}
202+
}
203+
}
204+
205+
var expected =
206+
"public class TestClass"
207+
+ Environment.NewLine
208+
+ " public void TestMethod()"
209+
+ Environment.NewLine
210+
+ " if (true)"
211+
+ Environment.NewLine
212+
+ " Console.WriteLine(\"Hello, World!\");";
213+
214+
_ = await Assert.That(builder.ToString()).IsEqualTo(expected);
215+
}
216+
217+
[Test]
218+
public async Task Scope_Should_Handle_Deep_Nesting()
219+
{
220+
var builder = new CSharpCodeBuilder();
221+
const int nestingLevels = 5;
222+
223+
// Create deep nesting
224+
var scopes = new ScopeHandler[nestingLevels];
225+
for (int i = 0; i < nestingLevels; i++)
226+
{
227+
scopes[i] = builder.Scope();
228+
_ = builder.AppendLine().Append($"Level {i + 1}");
229+
}
230+
231+
// Dispose scopes in reverse order (as would happen with nested using statements)
232+
for (int i = nestingLevels - 1; i >= 0; i--)
233+
{
234+
scopes[i].Dispose();
235+
}
236+
237+
_ = builder.AppendLine().Append("Back to level 0");
238+
239+
var expected =
240+
Environment.NewLine
241+
+ " Level 1"
242+
+ Environment.NewLine
243+
+ " Level 2"
244+
+ Environment.NewLine
245+
+ " Level 3"
246+
+ Environment.NewLine
247+
+ " Level 4"
248+
+ Environment.NewLine
249+
+ " Level 5"
250+
+ Environment.NewLine
251+
+ "Back to level 0";
252+
253+
_ = await Assert.That(builder.ToString()).IsEqualTo(expected);
254+
}
255+
256+
[Test]
257+
public async Task Scope_Disposal_Should_Be_Safe_To_Call_Multiple_Times()
258+
{
259+
var builder = new CSharpCodeBuilder();
260+
261+
var scope = builder.Scope();
262+
_ = builder.AppendLine().Append("test");
263+
264+
// Dispose multiple times should be safe
265+
scope.Dispose();
266+
scope.Dispose();
267+
scope.Dispose();
268+
269+
_ = builder.AppendLine().Append("after");
270+
271+
var expected = Environment.NewLine + " test" + Environment.NewLine + "after";
272+
_ = await Assert.That(builder.ToString()).IsEqualTo(expected);
273+
}
274+
}

0 commit comments

Comments
 (0)