From 905722f3edef8609bc12a17ae30b1adfb69c6a66 Mon Sep 17 00:00:00 2001 From: walterlv Date: Mon, 12 Jan 2026 09:03:04 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E4=BD=BF=E5=88=86=E6=9E=90=E5=99=A8?= =?UTF-8?q?=E5=92=8C=E6=9E=84=E5=BB=BA=E5=91=BD=E5=90=8D=E7=A9=BA=E9=97=B4?= =?UTF-8?q?=E7=9A=84=E4=BC=A0=E9=80=92=E6=96=B9=E5=BC=8F=E7=9B=B8=E5=90=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Directory.Build.props | 2 +- src/DotNetCampus.CommandLine/DotNetCampus.CommandLine.csproj | 2 +- .../Package/{build => buildTransitive}/Package.props | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename src/DotNetCampus.CommandLine/Package/{build => buildTransitive}/Package.props (100%) diff --git a/Directory.Build.props b/Directory.Build.props index 492000f..e40b48a 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -36,6 +36,6 @@ git - + diff --git a/src/DotNetCampus.CommandLine/DotNetCampus.CommandLine.csproj b/src/DotNetCampus.CommandLine/DotNetCampus.CommandLine.csproj index 3ca002a..078d0d9 100644 --- a/src/DotNetCampus.CommandLine/DotNetCampus.CommandLine.csproj +++ b/src/DotNetCampus.CommandLine/DotNetCampus.CommandLine.csproj @@ -47,7 +47,7 @@ - + diff --git a/src/DotNetCampus.CommandLine/Package/build/Package.props b/src/DotNetCampus.CommandLine/Package/buildTransitive/Package.props similarity index 100% rename from src/DotNetCampus.CommandLine/Package/build/Package.props rename to src/DotNetCampus.CommandLine/Package/buildTransitive/Package.props From 523e27b5395fa28d5f41b71b1a8f51f53d36f0d1 Mon Sep 17 00:00:00 2001 From: walterlv Date: Mon, 12 Jan 2026 09:49:48 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E6=94=AF=E6=8C=81=E9=9A=90=E5=BC=8F?= =?UTF-8?q?=E6=8E=A8=E6=96=AD=E7=B1=BB=E5=9E=8B=E7=9A=84=20AddHandler=20?= =?UTF-8?q?=E7=9A=84=E6=8B=A6=E6=88=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../InterceptorModelProvider.cs | 42 +++++++++++++++---- .../CommandMatching/AddHandlerTests.cs | 26 ++++++++++++ 2 files changed, 59 insertions(+), 9 deletions(-) diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/InterceptorModelProvider.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/InterceptorModelProvider.cs index 05a1be4..022b9d6 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/InterceptorModelProvider.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/InterceptorModelProvider.cs @@ -44,18 +44,31 @@ public static IncrementalValuesProvider SelectMethod { return context.SyntaxProvider.CreateSyntaxProvider((node, ct) => { - // 检查 commandLine.As() 方法调用。 + // 检查 commandLine.Xxx() 或 commandLine.Xxx((T o) => { }) 方法调用。 if (node is InvocationExpressionSyntax { Expression: MemberAccessExpressionSyntax { - Name: GenericNameSyntax - { - TypeArgumentList.Arguments.Count: 1, - } syntax, + Name: var nameSyntax, }, - } invocationExpressionNode && syntax.Identifier.Text == methodName) + } invocationExpressionNode) { + // 支持显式泛型参数(GenericNameSyntax)和隐式推断(SimpleNameSyntax/IdentifierNameSyntax)。 + var isMatch = nameSyntax switch + { + GenericNameSyntax + { + TypeArgumentList.Arguments.Count: 1, + } genericName => genericName.Identifier.Text == methodName, + not null => nameSyntax.Identifier.Text == methodName, + _ => false, + }; + + if (!isMatch) + { + return false; + } + // 再检查方法的参数列表是否是指定类型。 var expectedParameterCount = parameterTypeFullNameRegexes.Length; var argumentList = invocationExpressionNode.ArgumentList.Arguments; @@ -97,14 +110,25 @@ public static IncrementalValuesProvider SelectMethod } } - // 获取 commandLine.As() 中的 T。 - var genericTypeNode = ((GenericNameSyntax)((MemberAccessExpressionSyntax)node.Expression).Name).TypeArgumentList.Arguments[0]; - var symbol = ModelExtensions.GetSymbolInfo(c.SemanticModel, genericTypeNode, ct).Symbol as INamedTypeSymbol; + // 获取 commandLine.Xxx() 中的 T。 + // 支持从语法节点获取(显式泛型参数)或从方法符号获取(隐式推断)。 + var nameSyntax = ((MemberAccessExpressionSyntax)node.Expression).Name; + var symbol = nameSyntax switch + { + // commandLine.Xxx() 显式获取类型符号。 + GenericNameSyntax genericNameSyntax => ModelExtensions.GetSymbolInfo(c.SemanticModel, genericNameSyntax.TypeArgumentList.Arguments[0], ct) + .Symbol as INamedTypeSymbol, + // commandLine.Xxx((T o) => { }) 隐式获取类型符号。 + not null when methodSymbol.TypeArguments.Length == 1 => methodSymbol.TypeArguments[0] as INamedTypeSymbol, + _ => null, + }; + var interceptableLocation = c.SemanticModel.GetInterceptableLocation(node, ct); if (interceptableLocation is null || symbol is null) { return null; } + // 获取 [Command("xxx")] 或 [Verb("xxx")] 特性中的 xxx。 var commandAttribute = symbol.GetAttributes().FirstOrDefault(a => a.AttributeClass!.IsAttributeOf()) #pragma warning disable CS0618 // 类型或成员已过时 diff --git a/tests/DotNetCampus.CommandLine.Tests/CommandMatching/AddHandlerTests.cs b/tests/DotNetCampus.CommandLine.Tests/CommandMatching/AddHandlerTests.cs index a39b8b9..91e6a50 100644 --- a/tests/DotNetCampus.CommandLine.Tests/CommandMatching/AddHandlerTests.cs +++ b/tests/DotNetCampus.CommandLine.Tests/CommandMatching/AddHandlerTests.cs @@ -206,6 +206,32 @@ public void AddHandler_Mix3(string[] args, string expectedCommand, string expect Assert.AreEqual(1, exitCode); } + [TestMethod] + [DataRow(new[] { "foo" }, nameof(FooOptions), "Foo", TestCommandLineStyle.Flexible, DisplayName = "[Flexible] foo")] + [DataRow(new[] { "foo" }, nameof(FooOptions), "Foo", TestCommandLineStyle.DotNet, DisplayName = "[DotNet] foo")] + [DataRow(new[] { "foo" }, nameof(FooOptions), "Foo", TestCommandLineStyle.Gnu, DisplayName = "[Gnu] foo")] + [DataRow(new[] { "foo" }, nameof(FooOptions), "Foo", TestCommandLineStyle.Windows, DisplayName = "[Windows] foo")] + public async Task AddHandler_TypedDelegate(string[] args, string expectedCommand, string expectedValue, TestCommandLineStyle style) + { + // Arrange + string? matched = null; + var commandLine = CommandLine.Parse(args, style.ToParsingOptions()); + + // Act + var result = await commandLine + .AddHandler(async (FooOptions o) => + { + await Task.Yield(); + matched = o.Value; + }) + .RunAsync(); + var matchedTypeName = result.HandledBy!.GetType().Name; + + // Assert + Assert.AreEqual(expectedCommand, matchedTypeName); + Assert.AreEqual(expectedValue, matched); + } + // ReSharper disable once RedundantAssignment private int RunWithExitCode(ref T field, T value) { From e70e1f94eb175691ce999c54f3c52f6e11816850 Mon Sep 17 00:00:00 2001 From: walterlv Date: Mon, 12 Jan 2026 10:01:15 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E5=B0=A4=E5=85=B6=E8=A6=81=E4=BC=A0?= =?UTF-8?q?=E4=B8=80=E4=B8=AA=E9=BB=98=E8=AE=A4=E5=91=BD=E4=BB=A4=E5=A4=84?= =?UTF-8?q?=E7=90=86=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/DotNetCampus.CommandLine/CommandRunner.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DotNetCampus.CommandLine/CommandRunner.cs b/src/DotNetCampus.CommandLine/CommandRunner.cs index e5e8437..b14c1c2 100644 --- a/src/DotNetCampus.CommandLine/CommandRunner.cs +++ b/src/DotNetCampus.CommandLine/CommandRunner.cs @@ -89,7 +89,7 @@ public Task RunAsync() { throw new CommandNameNotFoundException( string.IsNullOrEmpty(possibleCommandNames) - ? "No command handler found. Please ensure that at least one command handler is registered by AddHandler()." + ? "No command handler found. Please ensure that at least one command handler is registered by AddHandler(), especially a default command handler." : $"No command handler found for command '{possibleCommandNames}'. Please ensure that the command handler is registered by AddHandler().", possibleCommandNames); }