diff --git a/EasyCodeBuilder.Test/Csharp/TryCatchFinallyTests.cs b/EasyCodeBuilder.Test/Csharp/TryCatchFinallyTests.cs new file mode 100644 index 0000000..dd25a1e --- /dev/null +++ b/EasyCodeBuilder.Test/Csharp/TryCatchFinallyTests.cs @@ -0,0 +1,288 @@ +using System; +using Fengb3.EasyCodeBuilder; +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 TryWithoutCatchOrFinallyThrows() + { + var tryOption = new TryOption(); + tryOption.AppendLine("DoWork();"); + + Assert.Throws(() => + { + var cb = new CodeBuilder(' ', 2, "\n{", "}", 1024); + tryOption.Build(cb); + }); + } + + [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] + 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..601d517 --- /dev/null +++ b/EasyCodeBuilder/Csharp/TryCatchFinallyOption.cs @@ -0,0 +1,189 @@ +using System; +using System.Collections.Generic; + +namespace Fengb3.EasyCodeBuilder.Csharp; + +/// +/// try 语句选项 +/// +public class TryOption : CodeOption +{ + private readonly List _catches = new List(); + private FinallyOption? _finally; + + /// + /// 构建代码 + /// + /// 代码构建器 + /// 代码构建器 + 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"); + foreach (var catchOption in _catches) + { + catchOption.Build(cb); + } + _finally?.Build(cb); + return cb; + } + + 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) + { + if (_finally is not null) + { + throw new InvalidOperationException("A try statement cannot have more than one finally clause."); + } + _finally = finallyOption; + } +} + +/// +/// 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) + { + if (configure is null) throw new ArgumentNullException(nameof(configure)); + var catchOption = new CatchOption(); + configure(catchOption); + tryOption.AddCatch(catchOption); + return tryOption; + } + + /// + /// add a finally clause to the try block + /// + /// try 语句选项 + /// finally 语句配置委托 + /// 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.SetFinally(finallyOption); + 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; + } +}