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.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/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); } 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 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) {