From e5cf3b1ac396910937708f0c01a0c661c88bf552 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Apr 2026 08:45:22 +0000 Subject: [PATCH 1/3] Initial plan From 494e254e833b523404279c22c62849ab947076ed Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Apr 2026 08:53:32 +0000 Subject: [PATCH 2/3] Add try-catch-finally support Agent-Logs-Url: https://github.com/fengb3/EasyCodeBuilder/sessions/6e5dcf18-3a2b-4da1-a078-791854f18069 Co-authored-by: fengb3 <45181245+fengb3@users.noreply.github.com> --- .../Csharp/TryCatchFinallyTests.cs | 264 ++++++++++++++++++ EasyCodeBuilder/Csharp/MethodOption.cs | 9 + .../Csharp/TryCatchFinallyOption.cs | 163 +++++++++++ 3 files changed, 436 insertions(+) create mode 100644 EasyCodeBuilder.Test/Csharp/TryCatchFinallyTests.cs create mode 100644 EasyCodeBuilder/Csharp/TryCatchFinallyOption.cs diff --git a/EasyCodeBuilder.Test/Csharp/TryCatchFinallyTests.cs b/EasyCodeBuilder.Test/Csharp/TryCatchFinallyTests.cs new file mode 100644 index 0000000..6771927 --- /dev/null +++ b/EasyCodeBuilder.Test/Csharp/TryCatchFinallyTests.cs @@ -0,0 +1,264 @@ +using Fengb3.EasyCodeBuilder.Csharp; +using Xunit.Abstractions; + +namespace EasyCodeBuilder.Test.Csharp; + +public class TryCatchFinallyTests(ITestOutputHelper output) +{ + private static string Norm(string text) => text.Replace("\r\n", "\n").Trim(); + + [Fact] + public void TryOnly() + { + var code = new MethodOption() + .WithKeyword("public") + .WithName("Run") + .WithReturnType("void") + .Try(@try => + { + @try.AppendLine("DoWork();"); + }) + .Build(); + + var expected = """ + public void Run() + { + try + { + DoWork(); + } + } + """; + + Assert.Equal(Norm(expected), Norm(code)); + output.WriteLine(code); + } + + [Fact] + public void TryCatch() + { + var code = new MethodOption() + .WithKeyword("public") + .WithName("Run") + .WithReturnType("void") + .Try(@try => + { + @try.AppendLine("DoWork();"); + @try.Catch(c => + { + c.ExceptionType = "Exception"; + c.VariableName = "ex"; + c.AppendLine("Console.WriteLine(ex.Message);"); + }); + }) + .Build(); + + var expected = """ + public void Run() + { + try + { + DoWork(); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + } + """; + + Assert.Equal(Norm(expected), Norm(code)); + output.WriteLine(code); + } + + [Fact] + public void TryCatchFinally() + { + var code = new MethodOption() + .WithKeyword("public") + .WithName("Run") + .WithReturnType("void") + .Try(@try => + { + @try.AppendLine("DoWork();"); + @try.Catch(c => + { + c.ExceptionType = "Exception"; + c.VariableName = "ex"; + c.AppendLine("Log(ex);"); + }); + @try.Finally(f => + { + f.AppendLine("Cleanup();"); + }); + }) + .Build(); + + var expected = """ + public void Run() + { + try + { + DoWork(); + } + catch (Exception ex) + { + Log(ex); + } + finally + { + Cleanup(); + } + } + """; + + Assert.Equal(Norm(expected), Norm(code)); + output.WriteLine(code); + } + + [Fact] + public void TryMultipleCatch() + { + var code = new MethodOption() + .WithKeyword("public") + .WithName("Run") + .WithReturnType("void") + .Try(@try => + { + @try.AppendLine("DoWork();"); + @try.Catch(c => + { + c.ExceptionType = "ArgumentNullException"; + c.VariableName = "ex"; + c.AppendLine("HandleNull(ex);"); + }); + @try.Catch(c => + { + c.ExceptionType = "InvalidOperationException"; + c.VariableName = "ex"; + c.AppendLine("HandleInvalid(ex);"); + }); + @try.Catch(c => + { + c.ExceptionType = "Exception"; + c.VariableName = "ex"; + c.AppendLine("HandleGeneral(ex);"); + }); + }) + .Build(); + + Assert.Contains("catch (ArgumentNullException ex)", code); + Assert.Contains("catch (InvalidOperationException ex)", code); + Assert.Contains("catch (Exception ex)", code); + Assert.Contains("HandleNull(ex);", code); + Assert.Contains("HandleInvalid(ex);", code); + Assert.Contains("HandleGeneral(ex);", code); + output.WriteLine(code); + } + + [Fact] + public void TryCatchWithoutVariableName() + { + var code = new MethodOption() + .WithKeyword("public") + .WithName("Run") + .WithReturnType("void") + .Try(@try => + { + @try.AppendLine("DoWork();"); + @try.Catch(c => + { + c.ExceptionType = "Exception"; + c.AppendLine("HandleError();"); + }); + }) + .Build(); + + Assert.Contains("catch (Exception)", code); + Assert.Contains("HandleError();", code); + output.WriteLine(code); + } + + [Fact] + public void BareCatch() + { + var code = new MethodOption() + .WithKeyword("public") + .WithName("Run") + .WithReturnType("void") + .Try(@try => + { + @try.AppendLine("DoWork();"); + @try.Catch(c => + { + c.AppendLine("HandleError();"); + }); + }) + .Build(); + + Assert.Contains("catch", code); + Assert.DoesNotContain("catch (", code); + Assert.Contains("HandleError();", code); + output.WriteLine(code); + } + + [Fact] + public void TryCatchWithWhenFilter() + { + var code = new MethodOption() + .WithKeyword("public") + .WithName("Run") + .WithReturnType("void") + .Try(@try => + { + @try.AppendLine("DoWork();"); + @try.Catch(c => + { + c.WithExceptionType("Exception") + .WithVariableName("ex") + .WithWhenFilter("ex.Message.Contains(\"timeout\")") + .AppendLine("HandleTimeout(ex);"); + }); + }) + .Build(); + + Assert.Contains("catch (Exception ex) when (ex.Message.Contains(\"timeout\"))", code); + Assert.Contains("HandleTimeout(ex);", code); + output.WriteLine(code); + } + + [Fact] + public void TryFinallyWithoutCatch() + { + var code = new MethodOption() + .WithKeyword("public") + .WithName("Run") + .WithReturnType("void") + .Try(@try => + { + @try.AppendLine("DoWork();"); + @try.Finally(f => + { + f.AppendLine("Cleanup();"); + }); + }) + .Build(); + + var expected = """ + public void Run() + { + try + { + DoWork(); + } + finally + { + Cleanup(); + } + } + """; + + Assert.Equal(Norm(expected), Norm(code)); + output.WriteLine(code); + } +} diff --git a/EasyCodeBuilder/Csharp/MethodOption.cs b/EasyCodeBuilder/Csharp/MethodOption.cs index 3a8620a..e9546ed 100644 --- a/EasyCodeBuilder/Csharp/MethodOption.cs +++ b/EasyCodeBuilder/Csharp/MethodOption.cs @@ -169,4 +169,13 @@ public static MethodOption Foreach(this MethodOption method, Action方法选项 public static MethodOption Switch(this MethodOption method, Action configure) => method.AddChild(configure); + + /// + /// add try-catch-finally statement + /// + /// 方法选项 + /// try 语句配置委托 + /// 方法选项 + public static MethodOption Try(this MethodOption method, Action configure) + => method.AddChild(configure); } \ No newline at end of file diff --git a/EasyCodeBuilder/Csharp/TryCatchFinallyOption.cs b/EasyCodeBuilder/Csharp/TryCatchFinallyOption.cs new file mode 100644 index 0000000..483251a --- /dev/null +++ b/EasyCodeBuilder/Csharp/TryCatchFinallyOption.cs @@ -0,0 +1,163 @@ +using System; + +namespace Fengb3.EasyCodeBuilder.Csharp; + +/// +/// try 语句选项 +/// +public class TryOption : CodeOption +{ + private CodeRenderFragment? _clauses; + + /// + /// 构建代码 + /// + /// 代码构建器 + /// 代码构建器 + public override CodeBuilder Build(CodeBuilder cb) + { + BeforeChildren?.Invoke(cb); + cb.CodeBlock(OnChildren, "try"); + _clauses?.Invoke(cb); + return cb; + } + + internal void AddClause(CodeRenderFragment clause) + { + _clauses += clause; + } +} + +/// +/// catch 语句选项 +/// +public class CatchOption : CodeOption +{ + /// + /// 捕获的异常类型(为空时表示裸 catch) + /// + public string ExceptionType { get; set; } = ""; + + /// + /// 异常变量名(可为空) + /// + public string VariableName { get; set; } = ""; + + /// + /// when 过滤条件(可为空) + /// + public string WhenFilter { get; set; } = ""; + + /// + /// 构建代码 + /// + /// 代码构建器 + /// 代码构建器 + public override CodeBuilder Build(CodeBuilder cb) + { + string header; + if (string.IsNullOrEmpty(ExceptionType)) + { + header = "catch"; + } + else if (string.IsNullOrEmpty(VariableName)) + { + header = $"catch ({ExceptionType})"; + } + else + { + header = $"catch ({ExceptionType} {VariableName})"; + } + + if (!string.IsNullOrEmpty(WhenFilter)) + { + header += $" when ({WhenFilter})"; + } + + return cb.CodeBlock(OnChildren, header); + } +} + +/// +/// finally 语句选项 +/// +public class FinallyOption : CodeOption +{ + /// + /// 构建代码 + /// + /// 代码构建器 + /// 代码构建器 + public override CodeBuilder Build(CodeBuilder cb) + => cb.CodeBlock(OnChildren, "finally"); +} + +/// +/// try-catch-finally 选项扩展方法 +/// +public static class TryCatchFinallyOptionExtensions +{ + /// + /// add a catch clause to the try block + /// + /// try 语句选项 + /// catch 语句配置委托 + /// try 语句选项 + public static TryOption Catch(this TryOption tryOption, Action configure) + { + var catchOption = new CatchOption(); + configure(catchOption); + tryOption.AddClause(catchOption.Build); + return tryOption; + } + + /// + /// add a finally clause to the try block + /// + /// try 语句选项 + /// finally 语句配置委托 + /// try 语句选项 + public static TryOption Finally(this TryOption tryOption, Action configure) + { + var finallyOption = new FinallyOption(); + configure(finallyOption); + tryOption.AddClause(finallyOption.Build); + return tryOption; + } + + /// + /// set exception type of catch clause + /// + /// catch 语句选项 + /// 异常类型 + /// catch 语句选项 + public static CatchOption WithExceptionType(this CatchOption catchOption, string exceptionType) + { + catchOption.ExceptionType = exceptionType; + return catchOption; + } + + /// + /// set variable name of catch clause + /// + /// catch 语句选项 + /// 异常变量名 + /// catch 语句选项 + public static CatchOption WithVariableName(this CatchOption catchOption, string variableName) + { + catchOption.VariableName = variableName; + return catchOption; + } + + /// + /// set when filter of catch clause + /// + /// catch 语句选项 + /// 过滤条件表达式 + /// catch 语句选项 + public static CatchOption WithWhenFilter(this CatchOption catchOption, string whenFilter) + { + catchOption.WhenFilter = whenFilter; + return catchOption; + } +} From e3f2c34a869e1e86cf19950ae1644403791c2fcd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Apr 2026 09:19:35 +0000 Subject: [PATCH 3/3] Apply review feedback: structured clause storage, validation, null-checks Agent-Logs-Url: https://github.com/fengb3/EasyCodeBuilder/sessions/fb7d96bc-d23c-4621-b3b9-a8f7ebb66416 Co-authored-by: fengb3 <45181245+fengb3@users.noreply.github.com> --- .../Csharp/TryCatchFinallyTests.cs | 66 +++++++++++++------ .../Csharp/TryCatchFinallyOption.cs | 38 +++++++++-- 2 files changed, 77 insertions(+), 27 deletions(-) diff --git a/EasyCodeBuilder.Test/Csharp/TryCatchFinallyTests.cs b/EasyCodeBuilder.Test/Csharp/TryCatchFinallyTests.cs index 6771927..dd25a1e 100644 --- a/EasyCodeBuilder.Test/Csharp/TryCatchFinallyTests.cs +++ b/EasyCodeBuilder.Test/Csharp/TryCatchFinallyTests.cs @@ -1,3 +1,5 @@ +using System; +using Fengb3.EasyCodeBuilder; using Fengb3.EasyCodeBuilder.Csharp; using Xunit.Abstractions; @@ -8,30 +10,52 @@ public class TryCatchFinallyTests(ITestOutputHelper output) private static string Norm(string text) => text.Replace("\r\n", "\n").Trim(); [Fact] - public void TryOnly() + public void TryWithoutCatchOrFinallyThrows() { - var code = new MethodOption() - .WithKeyword("public") - .WithName("Run") - .WithReturnType("void") - .Try(@try => - { - @try.AppendLine("DoWork();"); - }) - .Build(); + var tryOption = new TryOption(); + tryOption.AppendLine("DoWork();"); - var expected = """ - public void Run() - { - try - { - DoWork(); - } - } - """; + Assert.Throws(() => + { + var cb = new CodeBuilder(' ', 2, "\n{", "}", 1024); + tryOption.Build(cb); + }); + } - Assert.Equal(Norm(expected), Norm(code)); - output.WriteLine(code); + [Fact] + public void CatchAfterFinallyThrows() + { + Assert.Throws(() => + { + var tryOption = new TryOption(); + tryOption.Finally(f => f.AppendLine("Cleanup();")); + tryOption.Catch(c => c.AppendLine("Handle();")); + }); + } + + [Fact] + public void MultipleFinallyThrows() + { + Assert.Throws(() => + { + var tryOption = new TryOption(); + tryOption.Finally(f => f.AppendLine("Cleanup1();")); + tryOption.Finally(f => f.AppendLine("Cleanup2();")); + }); + } + + [Fact] + public void NullCatchConfigureThrows() + { + var tryOption = new TryOption(); + Assert.Throws(() => tryOption.Catch(null!)); + } + + [Fact] + public void NullFinallyConfigureThrows() + { + var tryOption = new TryOption(); + Assert.Throws(() => tryOption.Finally(null!)); } [Fact] diff --git a/EasyCodeBuilder/Csharp/TryCatchFinallyOption.cs b/EasyCodeBuilder/Csharp/TryCatchFinallyOption.cs index 483251a..601d517 100644 --- a/EasyCodeBuilder/Csharp/TryCatchFinallyOption.cs +++ b/EasyCodeBuilder/Csharp/TryCatchFinallyOption.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace Fengb3.EasyCodeBuilder.Csharp; @@ -7,7 +8,8 @@ namespace Fengb3.EasyCodeBuilder.Csharp; /// public class TryOption : CodeOption { - private CodeRenderFragment? _clauses; + private readonly List _catches = new List(); + private FinallyOption? _finally; /// /// 构建代码 @@ -16,15 +18,37 @@ public class TryOption : CodeOption /// 代码构建器 public override CodeBuilder Build(CodeBuilder cb) { + if (_catches.Count == 0 && _finally is null) + { + throw new InvalidOperationException("A try statement must include at least one catch or finally clause."); + } + BeforeChildren?.Invoke(cb); cb.CodeBlock(OnChildren, "try"); - _clauses?.Invoke(cb); + foreach (var catchOption in _catches) + { + catchOption.Build(cb); + } + _finally?.Build(cb); return cb; } - internal void AddClause(CodeRenderFragment clause) + internal void AddCatch(CatchOption catchOption) + { + if (_finally is not null) + { + throw new InvalidOperationException("A catch clause cannot appear after a finally clause."); + } + _catches.Add(catchOption); + } + + internal void SetFinally(FinallyOption finallyOption) { - _clauses += clause; + if (_finally is not null) + { + throw new InvalidOperationException("A try statement cannot have more than one finally clause."); + } + _finally = finallyOption; } } @@ -105,9 +129,10 @@ public static class TryCatchFinallyOptionExtensions /// try 语句选项 public static TryOption Catch(this TryOption tryOption, Action configure) { + if (configure is null) throw new ArgumentNullException(nameof(configure)); var catchOption = new CatchOption(); configure(catchOption); - tryOption.AddClause(catchOption.Build); + tryOption.AddCatch(catchOption); return tryOption; } @@ -119,9 +144,10 @@ public static TryOption Catch(this TryOption tryOption, Action conf /// try 语句选项 public static TryOption Finally(this TryOption tryOption, Action configure) { + if (configure is null) throw new ArgumentNullException(nameof(configure)); var finallyOption = new FinallyOption(); configure(finallyOption); - tryOption.AddClause(finallyOption.Build); + tryOption.SetFinally(finallyOption); return tryOption; }