diff --git a/eng/source-build-patches/0002-Use-package-version-properties-for-reference-version.patch b/eng/source-build-patches/0002-Use-package-version-properties-for-reference-version.patch index 4744fa2791..212b26b6af 100644 --- a/eng/source-build-patches/0002-Use-package-version-properties-for-reference-version.patch +++ b/eng/source-build-patches/0002-Use-package-version-properties-for-reference-version.patch @@ -27,21 +27,3 @@ index 3048701..16a0eb7 100644 + 4.5.4 + -diff --git a/src/System.CommandLine/System.CommandLine.csproj b/src/System.CommandLine/System.CommandLine.csproj -index 1ae8dd3..aaa2c4a 100644 ---- a/src/System.CommandLine/System.CommandLine.csproj -+++ b/src/System.CommandLine/System.CommandLine.csproj -@@ -19,8 +19,8 @@ - - - -- -- -+ -+ - - - --- -2.18.0 - diff --git a/eng/source-build-patches/0003-Update-to-netcoreapp3.1-to-avoid-prebuilts.patch b/eng/source-build-patches/0003-Update-to-netcoreapp3.1-to-avoid-prebuilts.patch index 733c404a70..9de8b2b8e5 100644 --- a/eng/source-build-patches/0003-Update-to-netcoreapp3.1-to-avoid-prebuilts.patch +++ b/eng/source-build-patches/0003-Update-to-netcoreapp3.1-to-avoid-prebuilts.patch @@ -16,20 +16,6 @@ Includes a code fix for ref nullability with the new framework. src/System.CommandLine/System.CommandLine.csproj | 7 +------ 6 files changed, 7 insertions(+), 12 deletions(-) -diff --git a/src/System.CommandLine.Generator/System.CommandLine.Generator.csproj b/src/System.CommandLine.Generator/System.CommandLine.Generator.csproj -index d419906e..8644194a 100644 ---- a/src/System.CommandLine.Generator/System.CommandLine.Generator.csproj -+++ b/src/System.CommandLine.Generator/System.CommandLine.Generator.csproj -@@ -1,7 +1,7 @@ -- -+ - - -- netstandard2.0 -+ net5.0 - enable - 9.0 - true diff --git a/src/System.CommandLine.DragonFruit/System.CommandLine.DragonFruit.csproj b/src/System.CommandLine.DragonFruit/System.CommandLine.DragonFruit.csproj index b3a542fd..0e49d818 100644 --- a/src/System.CommandLine.DragonFruit/System.CommandLine.DragonFruit.csproj @@ -100,8 +86,8 @@ index 125e9c78..3a10c14d 100644 - -- -- +- +- - - diff --git a/repack.ps1 b/repack.ps1 new file mode 100644 index 0000000000..9297374748 --- /dev/null +++ b/repack.ps1 @@ -0,0 +1,11 @@ + +# clean up the previously-cached NuGet packages +Remove-Item -Recurse ~\.nuget\packages\System.CommandLine* -Force + +# build and pack dotnet-interactive +dotnet clean +dotnet pack /p:PackageVersion=2.0.0-dev + +# copy the dotnet-interactive packages to the temp directory +Get-ChildItem -Recurse -Filter *.nupkg | Copy-Item -Destination c:\temp -Force + diff --git a/src/System.CommandLine.Generator.Tests/GeneratedCommandHandlerTests.cs b/src/System.CommandLine.Generator.Tests/GeneratedCommandHandlerTests.cs index 6db98b2830..3b4b886cd4 100644 --- a/src/System.CommandLine.Generator.Tests/GeneratedCommandHandlerTests.cs +++ b/src/System.CommandLine.Generator.Tests/GeneratedCommandHandlerTests.cs @@ -9,9 +9,7 @@ using System.Threading.Tasks; using FluentAssertions; using Xunit; -using static System.CommandLine.Invocation.CommandHandlerGenerator; -#nullable enable namespace System.CommandLine.Generator.Tests { public class CommandHandlerTests @@ -32,13 +30,16 @@ void Execute(string fullnameOrNickname, IConsole console, int age) boundAge = age; } - var command = new Command("command"); var nameArgument = new Argument(); - command.AddArgument(nameArgument); var ageOption = new Option("--age"); - command.AddOption(ageOption); - command.Handler = GeneratedHandler.Create> + var command = new Command("command") + { + nameArgument, + ageOption + }; + + command.SetHandler> (Execute, nameArgument, ageOption); await command.InvokeAsync("command Gandalf --age 425", _console); @@ -46,32 +47,33 @@ void Execute(string fullnameOrNickname, IConsole console, int age) boundName.Should().Be("Gandalf"); boundAge.Should().Be(425); boundConsole.Should().NotBeNull(); - } - + } + [Fact] - public async Task Can_generate_handler_for_method_with_model() + public async Task Can_generate_handler_for_void_returning_delegate() { string? boundName = default; int boundAge = default; IConsole? boundConsole = null; - void Execute(Character character, IConsole console) - { - boundName = character.FullName; - boundConsole = console; - boundAge = character.Age; - } - - var command = new Command("command"); - var nameOption = new Option("--name"); - command.AddOption(nameOption); + var nameArgument = new Argument(); var ageOption = new Option("--age"); - command.AddOption(ageOption); - command.Handler = GeneratedHandler.Create> - (Execute, nameOption, ageOption); + var command = new Command("command") + { + nameArgument, + ageOption + }; - await command.InvokeAsync("command --age 425 --name Gandalf", _console); + command.SetHandler> + ((fullnameOrNickname, console, age) => + { + boundName = fullnameOrNickname; + boundConsole = console; + boundAge = age; + }, nameArgument, ageOption); + + await command.InvokeAsync("command Gandalf --age 425", _console); boundName.Should().Be("Gandalf"); boundAge.Should().Be(425); @@ -79,7 +81,7 @@ void Execute(Character character, IConsole console) } [Fact] - public async Task Can_generate_handler_for_method_with_model_property_binding() + public async Task Can_generate_handler_for_method_with_model() { string? boundName = default; int boundAge = default; @@ -98,12 +100,7 @@ void Execute(Character character, IConsole console) var ageOption = new Option("--age"); command.AddOption(ageOption); - command.Handler = GeneratedHandler.Create, Character> - (Execute, context => new Character - { - FullName = context.ParseResult.ValueForOption(nameOption), - Age = context.ParseResult.ValueForOption(ageOption), - }); + command.SetHandler>(Execute, nameOption, ageOption); await command.InvokeAsync("command --age 425 --name Gandalf", _console); @@ -126,8 +123,7 @@ int Execute(int first, int second) var secondArgument = new Argument("second"); command.AddArgument(secondArgument); - command.Handler = GeneratedHandler.Create> - (Execute, firstArgument, secondArgument); + command.SetHandler>(Execute, firstArgument, secondArgument); int result = await command.InvokeAsync("add 1 2", _console); @@ -145,7 +141,7 @@ public async Task Can_generate_handler_with_well_know_parameters_types() void Execute( InvocationContext invocationContext, - IConsole console, + IConsole console, ParseResult parseResult, IHelpBuilder helpBuilder, BindingContext bindingContext) @@ -159,7 +155,7 @@ void Execute( var command = new Command("command"); - command.Handler = GeneratedHandler.Create>(Execute); + command.SetHandler>(Execute); await command.InvokeAsync("command", _console); @@ -179,20 +175,22 @@ public async Task Can_generate_handler_for_async_method() async Task ExecuteAsync(string fullnameOrNickname, IConsole console, int age) { - //Just long enough to make sure the taks is be awaited - await Task.Delay(100); + await Task.Yield(); boundName = fullnameOrNickname; boundConsole = console; boundAge = age; } - var command = new Command("command"); var nameArgument = new Argument(); - command.AddArgument(nameArgument); var ageOption = new Option("--age"); - command.AddOption(ageOption); - command.Handler = GeneratedHandler.Create> + var command = new Command("command") + { + nameArgument, + ageOption + }; + + command.SetHandler> (ExecuteAsync, nameArgument, ageOption); await command.InvokeAsync("command Gandalf --age 425", _console); @@ -207,50 +205,52 @@ public async Task Can_generate_handler_for_async_task_of_int_returning_method() { async Task Execute(int first, int second) { - await Task.Delay(100); + await Task.Yield(); return first + second; } - var command = new Command("add"); var firstArgument = new Argument("first"); - command.AddArgument(firstArgument); var secondArgument = new Argument("second"); - command.AddArgument(secondArgument); + var command = new Command("add") + { + firstArgument, + secondArgument + }; - command.Handler = GeneratedHandler.Create>> + command.SetHandler>> (Execute, firstArgument, secondArgument); int result = await command.InvokeAsync("add 1 2", _console); result.Should().Be(3); - } + } [Fact] public async Task Can_generate_handler_for_multiple_commands_with_the_same_signature() { string firstValue = ""; + void Execute1(string value) { firstValue = value; } + string secondValue = ""; + void Execute2(string value) { secondValue = value; } - var command1 = new Command("first"); var argument1 = new Argument("first-value"); command1.AddArgument(argument1); - command1.Handler = GeneratedHandler.Create> - (Execute1, argument1); + command1.SetHandler>(Execute1, argument1); var command2 = new Command("second"); var argument2 = new Argument("second-value"); command2.AddArgument(argument2); - command2.Handler = GeneratedHandler.Create> - (Execute2, argument2); + command2.SetHandler>(Execute2, argument2); await command1.InvokeAsync("first v1", _console); await command2.InvokeAsync("second v2", _console); @@ -268,12 +268,11 @@ public Character(string? fullName, int age) } public Character() - { } + { + } public string? FullName { get; set; } public int Age { get; set; } } - } } -#nullable restore diff --git a/src/System.CommandLine.Generator.Tests/System.CommandLine.Generator.Tests.csproj b/src/System.CommandLine.Generator.Tests/System.CommandLine.Generator.Tests.csproj index a28060ab35..25e7d2f169 100644 --- a/src/System.CommandLine.Generator.Tests/System.CommandLine.Generator.Tests.csproj +++ b/src/System.CommandLine.Generator.Tests/System.CommandLine.Generator.Tests.csproj @@ -5,6 +5,7 @@ 9 true true + enable diff --git a/src/System.CommandLine.Generator/CommandHandlerGenerator.cs b/src/System.CommandLine.Generator/CommandHandlerGenerator.cs deleted file mode 100644 index 24c59f36b2..0000000000 --- a/src/System.CommandLine.Generator/CommandHandlerGenerator.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace System.CommandLine.Invocation -{ - public sealed class CommandHandlerGenerator - { - private CommandHandlerGenerator() - { - } - - public static CommandHandlerGenerator GeneratedHandler { get; } = null!; - } -} \ No newline at end of file diff --git a/src/System.CommandLine.Generator/CommandHandlerGeneratorExtensions.cs b/src/System.CommandLine.Generator/CommandHandlerGeneratorExtensions.cs deleted file mode 100644 index a6fd9dd257..0000000000 --- a/src/System.CommandLine.Generator/CommandHandlerGeneratorExtensions.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace System.CommandLine.Invocation -{ - public static class CommandHandlerGeneratorExtensions - { - public static ICommandHandler Create(this CommandHandlerGenerator handler, - TDelegate @delegate, params ISymbol[] symbols) - where TDelegate : Delegate - { - //TODO: Better exception/message - throw new InvalidOperationException("Should never get here...."); - } - - public static ICommandHandler Create(this CommandHandlerGenerator handler, - TDelegate @delegate, Func modelBuilder) - where TDelegate : Delegate - { - //TODO: Better exception/message - throw new InvalidOperationException("Should never get here...."); - } - } -} \ No newline at end of file diff --git a/src/System.CommandLine.Generator/CommandHandlerSourceGenerator.cs b/src/System.CommandLine.Generator/CommandHandlerSourceGenerator.cs index 064c16e896..57fc272943 100644 --- a/src/System.CommandLine.Generator/CommandHandlerSourceGenerator.cs +++ b/src/System.CommandLine.Generator/CommandHandlerSourceGenerator.cs @@ -1,7 +1,6 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using System.CommandLine.Invocation; using System.Linq; using System.Text; using Microsoft.CodeAnalysis; @@ -17,19 +16,29 @@ public void Execute(GeneratorExecutionContext context) { SyntaxReceiver rx = (SyntaxReceiver)context.SyntaxContextReceiver!; + if (rx.Invocations.Count == 0) + { + return; + } + StringBuilder builder = new(); builder.Append( $@"// Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.ComponentModel; using System.CommandLine.Binding; using System.Reflection; using System.Threading.Tasks; using System.CommandLine.Invocation; -namespace {typeof(CommandHandlerGeneratorExtensions).Namespace} +#pragma warning disable + +namespace System.CommandLine {{ - public static partial class CommandHandlerGeneratorExtensions_Generated + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + internal static class GeneratedCommandHandlers {{ "); int handlerCount = 1; @@ -43,8 +52,8 @@ public static partial class CommandHandlerGeneratorExtensions_Generated builder.Append( @$" - public static {ICommandHandlerType} {nameof(CommandHandlerGeneratorExtensions.Create)}<{string.Join(", ", Enumerable.Range(1, invocation.NumberOfGenerericParameters).Select(x => $@"T{x}"))}>( - this {nameof(CommandHandlerGenerator)} handler,"); + public static void SetHandler<{string.Join(", ", Enumerable.Range(1, invocation.NumberOfGenerericParameters).Select(x => $@"T{x}"))}>( + this Command command,"); builder.Append($@" {invocation.DelegateType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)} method"); @@ -62,7 +71,7 @@ public static partial class CommandHandlerGeneratorExtensions_Generated builder.Append(@" {"); builder.Append($@" - return new GeneratedHandler_{handlerCount}(method"); + command.Handler = new GeneratedHandler_{handlerCount}(method"); if (methodParameters.Length > 0) { @@ -138,6 +147,8 @@ public async Task InvokeAsync(InvocationContext context) public void Initialize(GeneratorInitializationContext context) { + // Debugger.Launch(); + context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); } } diff --git a/src/System.CommandLine.Generator/Invocations/FactoryModelBindingInvocation.cs b/src/System.CommandLine.Generator/Invocations/FactoryModelBindingInvocation.cs deleted file mode 100644 index db5b7380b9..0000000000 --- a/src/System.CommandLine.Generator/Invocations/FactoryModelBindingInvocation.cs +++ /dev/null @@ -1,78 +0,0 @@ -using Microsoft.CodeAnalysis; -using System.CommandLine.Generator.Parameters; -using System.Linq; -using System.Text; - -namespace System.CommandLine.Generator.Invocations -{ - internal class FactoryModelBindingInvocation : DelegateInvocation, IEquatable - { - public FactoryModelBindingInvocation( - ITypeSymbol delegateType, - ReturnPattern returnPattern) - : base(delegateType, returnPattern, 2) - { } - - public override string InvokeContents() - { - StringBuilder builder = new(); - - var factoryParam = (FactoryParameter)Parameters[0]; - builder.AppendLine($@" - var model = {factoryParam.LocalName}.Invoke(context);"); - - switch (ReturnPattern) - { - case ReturnPattern.FunctionReturnValue: - case ReturnPattern.AwaitFunction: - case ReturnPattern.AwaitFunctionReturnValue: - builder.Append(@" - var rv = "); - break; - } - builder.Append(@" - Method.Invoke(model"); - var remainigParameters = Parameters.Skip(1).ToList(); - if (remainigParameters.Count > 0) - { - builder.Append(", "); - builder.Append(string.Join(", ", remainigParameters.Select(x => x.GetValueFromContext()))); - } - builder.AppendLine(");"); - switch (ReturnPattern) - { - case ReturnPattern.InvocationContextExitCode: - builder.Append(@" - return await Task.FromResult(context.ExitCode);"); - break; - case ReturnPattern.FunctionReturnValue: - builder.Append(@" - return await Task.FromResult(rv);"); - break; - case ReturnPattern.AwaitFunction: - builder.Append(@" - await rv;"); - builder.Append(@" - return context.ExitCode;"); - break; - case ReturnPattern.AwaitFunctionReturnValue: - builder.Append(@" - return await rv;"); - break; - } - return builder.ToString(); - } - - public override int GetHashCode() - => base.GetHashCode(); - - public override bool Equals(object? obj) - => Equals(obj as FactoryModelBindingInvocation); - - public bool Equals(FactoryModelBindingInvocation? other) - { - if (other is null) return false; - return base.Equals(other); - } - } -} diff --git a/src/System.CommandLine.Generator/Parameters/FactoryParameter.cs b/src/System.CommandLine.Generator/Parameters/FactoryParameter.cs deleted file mode 100644 index 9e11bb8a73..0000000000 --- a/src/System.CommandLine.Generator/Parameters/FactoryParameter.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Microsoft.CodeAnalysis; - -namespace System.CommandLine.Generator.Parameters -{ - internal class FactoryParameter : Parameter, IEquatable - { - public ITypeSymbol FactoryType { get; } - public string LocalName { get; } - private string ParameterName => LocalName.ToLowerInvariant(); - - public FactoryParameter(RawParameter rawParameter, ITypeSymbol factoryType) - : base(rawParameter.ValueType) - { - LocalName = rawParameter.LocalName; - FactoryType = factoryType; - } - - public override string GetValueFromContext() => ""; - - public override string GetPropertyDeclaration() - => $"private {FactoryType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)} {LocalName} {{ get; }}"; - - public override string GetPropertyAssignment() - => $"{LocalName} = {ParameterName};"; - - public override (string Type, string Name) GetMethodParameter() - => (FactoryType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), ParameterName); - - public override int GetHashCode() - { - return base.GetHashCode() * -1521134295 + - SymbolComparer.GetHashCode(FactoryType) * -1521134295 + - HashCode(LocalName); - } - - public override bool Equals(object? obj) - => Equals(obj as FactoryParameter); - - public bool Equals(FactoryParameter? other) - { - return base.Equals(other) && - SymbolComparer.Equals(FactoryType, other.FactoryType) && - Equals(LocalName, other.LocalName); - } - } -} \ No newline at end of file diff --git a/src/System.CommandLine.Generator/SyntaxReceiver.cs b/src/System.CommandLine.Generator/SyntaxReceiver.cs index 32aa11605c..3f35373e41 100644 --- a/src/System.CommandLine.Generator/SyntaxReceiver.cs +++ b/src/System.CommandLine.Generator/SyntaxReceiver.cs @@ -1,144 +1,146 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; using System.Collections.Generic; using System.CommandLine.Generator.Invocations; using System.CommandLine.Generator.Parameters; -using System.CommandLine.Invocation; using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; namespace System.CommandLine.Generator { internal class SyntaxReceiver : ISyntaxContextReceiver { + private static readonly string _nameOfExtensionMethodAnchorType = "global::System.CommandLine.Command"; + public HashSet Invocations { get; } = new(); public void OnVisitSyntaxNode(GeneratorSyntaxContext context) { - if (context.Node is InvocationExpressionSyntax { Expression: MemberAccessExpressionSyntax memberAccess } invocationExpression && - memberAccess.Name.Identifier.Text == nameof(CommandHandlerGeneratorExtensions.Create) && - context.SemanticModel.GetSymbolInfo(invocationExpression) is { Symbol: IMethodSymbol invokeMethodSymbol } && - invokeMethodSymbol.ReceiverType?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == - $"global::{typeof(CommandHandlerGenerator).Namespace}.{nameof(CommandHandlerGenerator)}" && - invokeMethodSymbol.TypeArguments.Length is 1 or 2) + if (context.Node is not InvocationExpressionSyntax { Expression: MemberAccessExpressionSyntax memberAccess } invocationExpression) { - SymbolEqualityComparer symbolEqualityComparer = SymbolEqualityComparer.Default; - WellKnownTypes wellKnownTypes = new(context.SemanticModel.Compilation, symbolEqualityComparer); + return; + } - var delegateParameters = Array.Empty(); + if (memberAccess.Name.Identifier.Text != "SetHandler") + { + return; + } - //Check for model binding condition - if (invokeMethodSymbol.TypeArguments[0] is INamedTypeSymbol { TypeArguments: { Length: > 0 } } namedDelegateType) - { - if (namedDelegateType.DelegateInvokeMethod?.ReturnsVoid == false) - { - delegateParameters = namedDelegateType.TypeArguments - .Take(namedDelegateType.TypeArguments.Length - 1) - .Cast() - .ToArray(); - } - else - { - delegateParameters = namedDelegateType.TypeArguments - .Cast() - .ToArray(); - } - } + if (context.SemanticModel.GetSymbolInfo(invocationExpression) is not { Symbol: IMethodSymbol invokeMethodSymbol }) + { + return; + } - var symbols = invocationExpression.ArgumentList.Arguments - .Skip(1) - .Select(x => context.SemanticModel.GetSymbolInfo(x.Expression).Symbol) - .ToArray(); + if (invokeMethodSymbol.ReceiverType?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) != _nameOfExtensionMethodAnchorType) + { + return; + } + + if (invokeMethodSymbol.TypeArguments.Length is not 1) + { + return; + } + + SymbolEqualityComparer symbolEqualityComparer = SymbolEqualityComparer.Default; + WellKnownTypes wellKnownTypes = new(context.SemanticModel.Compilation, symbolEqualityComparer); + + var delegateParameters = Array.Empty(); - if (symbols.Any(x => x is null)) + //Check for model binding condition + if (invokeMethodSymbol.TypeArguments[0] is INamedTypeSymbol { TypeArguments: { Length: > 0 } } namedDelegateType) + { + if (namedDelegateType.DelegateInvokeMethod?.ReturnsVoid == false) + { + delegateParameters = namedDelegateType.TypeArguments + .Take(namedDelegateType.TypeArguments.Length - 1) + .Cast() + .ToArray(); + } + else { - return; + delegateParameters = namedDelegateType.TypeArguments + .Cast() + .ToArray(); } + } + + var symbols = invocationExpression.ArgumentList + .Arguments + .Skip(1) + .Select(x => context.SemanticModel.GetSymbolInfo(x.Expression).Symbol) + .ToArray(); + + if (symbols.Any(x => x is null)) + { + return; + } - IReadOnlyList givenParameters = GetParameters(symbols!); + IReadOnlyList givenParameters = GetParameters(symbols!); + ITypeSymbol delegateType = invokeMethodSymbol.TypeArguments[0]; + ReturnPattern returnPattern = GetReturnPattern(delegateType, context.SemanticModel.Compilation); - if (invokeMethodSymbol.TypeArguments.Length == 2) + if (IsMatch(delegateParameters, givenParameters, wellKnownTypes)) + { + + var invocation = new DelegateInvocation(delegateType, returnPattern, 1); + foreach (var parameter in PopulateParameters(delegateParameters, givenParameters, wellKnownTypes)) { - var rawParameter = (RawParameter)givenParameters[0]; - var factoryParameter = new FactoryParameter(rawParameter, invokeMethodSymbol.Parameters[1].Type); - givenParameters = new Parameter[] { factoryParameter }.Concat(givenParameters.Skip(1)).ToList(); + invocation.Parameters.Add(parameter); } - ITypeSymbol delegateType = invokeMethodSymbol.TypeArguments[0]; - ReturnPattern returnPattern = GetReturnPattern(delegateType, context.SemanticModel.Compilation); - - - if (IsMatch(delegateParameters, givenParameters, wellKnownTypes)) + Invocations.Add(invocation); + } + else if (delegateParameters[0] is INamedTypeSymbol modelType) + { + foreach (var ctor in modelType.Constructors.OrderByDescending(x => x.Parameters.Length)) { - if (invokeMethodSymbol.TypeArguments.Length == 2) - { - var invocation = new FactoryModelBindingInvocation(delegateType, returnPattern); - foreach (var parameter in PopulateParameters(delegateParameters, givenParameters, wellKnownTypes)) - { - invocation.Parameters.Add(parameter); - } - Invocations.Add(invocation); - } - else + var targetTypes = + ctor.Parameters.Select(x => x.Type) + .Concat(delegateParameters.Skip(1)) + .ToArray(); + if (IsMatch(targetTypes, givenParameters, wellKnownTypes)) { - var invocation = new DelegateInvocation(delegateType, returnPattern, 1); - foreach (var parameter in PopulateParameters(delegateParameters, givenParameters, wellKnownTypes)) + var invocation = new ConstructorModelBindingInvocation(ctor, returnPattern, delegateType); + foreach (var parameter in PopulateParameters(targetTypes, givenParameters, wellKnownTypes)) { invocation.Parameters.Add(parameter); } + Invocations.Add(invocation); + break; } } - else if (delegateParameters[0] is INamedTypeSymbol modelType) + } + + static bool IsMatch( + IReadOnlyList targetSymbols, + IReadOnlyList providedSymbols, + WellKnownTypes knownTypes) + { + SymbolEqualityComparer symbolEqualityComparer = SymbolEqualityComparer.Default; + int j = 0; + for (int i = 0; i < targetSymbols.Count; i++) { - foreach (var ctor in modelType.Constructors.OrderByDescending(x => x.Parameters.Length)) + if (j < providedSymbols.Count && + symbolEqualityComparer.Equals(providedSymbols[j].ValueType, targetSymbols[i])) { - var targetTypes = - ctor.Parameters.Select(x => x.Type) - .Concat(delegateParameters.Skip(1)) - .ToList(); - if (IsMatch(targetTypes, givenParameters, wellKnownTypes)) - { - var invocation = new ConstructorModelBindingInvocation(ctor, returnPattern, delegateType); - foreach (var parameter in PopulateParameters(targetTypes, givenParameters, wellKnownTypes)) - { - invocation.Parameters.Add(parameter); - } - Invocations.Add(invocation); - break; - } + j++; + //TODO: Handle the case where there are more provided symbols than needed } - } - - static bool IsMatch( - IReadOnlyList targetSymbols, - IReadOnlyList providedSymbols, - WellKnownTypes knownTypes) - { - SymbolEqualityComparer symbolEqualityComparer = SymbolEqualityComparer.Default; - int j = 0; - for (int i = 0; i < targetSymbols.Count; i++) + else if (!knownTypes.Contains(targetSymbols[i])) { - if (j < providedSymbols.Count && - symbolEqualityComparer.Equals(providedSymbols[j].ValueType, targetSymbols[i])) - { - j++; - //TODO: Handle the case where there are more provided symbols than needed - } - else if (!knownTypes.Contains(targetSymbols[i])) - { - return false; - } + return false; } - return j == providedSymbols.Count; } + + return j == providedSymbols.Count; } } private static IReadOnlyList PopulateParameters( - IReadOnlyList symbols, + IReadOnlyList symbols, IReadOnlyList givenParameters, WellKnownTypes knownTypes) { @@ -150,22 +152,24 @@ private static IReadOnlyList PopulateParameters( parameters.Insert(i, parameter!); } } + return parameters; } - private static IReadOnlyList GetParameters(IEnumerable symbols) + private static IReadOnlyList GetParameters(IEnumerable symbols) { List parameters = new(); int parameterIndex = 1; - foreach (Microsoft.CodeAnalysis.ISymbol symbol in symbols) + foreach (ISymbol symbol in symbols) { parameters.Add(GetParameter(symbol, $"Param{parameterIndex}")); parameterIndex++; } + return parameters; } - private static Parameter GetParameter(Microsoft.CodeAnalysis.ISymbol argumentSymbol, string localName) + private static Parameter GetParameter(ISymbol argumentSymbol, string localName) { return argumentSymbol switch { @@ -179,15 +183,17 @@ Parameter FromNamedTypeSymbol(INamedTypeSymbol namedTypeSymbol) { if (namedTypeSymbol.TypeArguments.Length > 0) { - if (namedTypeSymbol.Name == nameof(Option)) + if (namedTypeSymbol.Name == "Option") { return new OptionParameter(localName, namedTypeSymbol, namedTypeSymbol.TypeArguments[0]); } - else if (namedTypeSymbol.Name == nameof(Argument)) + + if (namedTypeSymbol.Name == "Argument") { return new ArgumentParameter(localName, namedTypeSymbol, namedTypeSymbol.TypeArguments[0]); } } + return new RawParameter(localName, namedTypeSymbol); } @@ -204,9 +210,7 @@ Parameter FromTypeSymbol(ITypeSymbol typeSymbol) private static ReturnPattern GetReturnPattern(ITypeSymbol delegateType, Compilation compilation) { ITypeSymbol? returnType = null; - if (delegateType is INamedTypeSymbol namedSymbol && - namedSymbol.DelegateInvokeMethod is { } delegateInvokeMethod && - !delegateInvokeMethod.ReturnsVoid) + if (delegateType is INamedTypeSymbol { DelegateInvokeMethod: { ReturnsVoid: false } delegateInvokeMethod }) { returnType = delegateInvokeMethod.ReturnType; } @@ -217,23 +221,23 @@ namedSymbol.DelegateInvokeMethod is { } delegateInvokeMethod && } SymbolEqualityComparer symbolEqualityComparer = SymbolEqualityComparer.Default; - + INamedTypeSymbol intType = compilation.GetSpecialType(SpecialType.System_Int32); if (symbolEqualityComparer.Equals(returnType, intType)) { return ReturnPattern.FunctionReturnValue; } - //TODO: what about toher awaiatables? + //TODO: what about other awaitables? INamedTypeSymbol taskType = compilation.GetTypeByMetadataName("System.Threading.Tasks.Task") - ?? throw new InvalidOperationException("Failed to find Task"); + ?? throw new InvalidOperationException("Failed to find Task"); if (symbolEqualityComparer.Equals(returnType, taskType)) { return ReturnPattern.AwaitFunction; } INamedTypeSymbol taskOfTType = compilation.GetTypeByMetadataName("System.Threading.Tasks.Task`1") - ?? throw new InvalidOperationException("Failed to find Task"); + ?? throw new InvalidOperationException("Failed to find Task"); if (returnType is INamedTypeSymbol { TypeArguments: { Length: 1 } } namedReturnType && symbolEqualityComparer.Equals(namedReturnType.TypeArguments[0], intType) && symbolEqualityComparer.Equals(namedReturnType.ConstructUnboundGenericType(), taskOfTType.ConstructUnboundGenericType())) @@ -245,4 +249,4 @@ namedSymbol.DelegateInvokeMethod is { } delegateInvokeMethod && return ReturnPattern.InvocationContextExitCode; } } -} +} \ No newline at end of file diff --git a/src/System.CommandLine.Generator/System.CommandLine.Generator.csproj b/src/System.CommandLine.Generator/System.CommandLine.Generator.csproj index b25c3ed5b1..6ffa8a90fa 100644 --- a/src/System.CommandLine.Generator/System.CommandLine.Generator.csproj +++ b/src/System.CommandLine.Generator/System.CommandLine.Generator.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 @@ -7,7 +7,6 @@ true false $(NoWarn);NU5128 - false @@ -24,8 +23,4 @@ - - - - diff --git a/src/System.CommandLine.Generator/WellKnownTypes.cs b/src/System.CommandLine.Generator/WellKnownTypes.cs index 38b07997d6..bfddbee3ab 100644 --- a/src/System.CommandLine.Generator/WellKnownTypes.cs +++ b/src/System.CommandLine.Generator/WellKnownTypes.cs @@ -1,9 +1,9 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using Microsoft.CodeAnalysis; using System.Collections.Generic; using System.CommandLine.Generator.Parameters; +using Microsoft.CodeAnalysis; namespace System.CommandLine.Generator { @@ -14,9 +14,9 @@ internal class WellKnownTypes public INamedTypeSymbol InvocationContext { get; } public INamedTypeSymbol HelpBuilder { get; } public INamedTypeSymbol BindingContext { get; } - public IEqualityComparer Comparer { get; } + public IEqualityComparer Comparer { get; } - public WellKnownTypes(Compilation compilation, IEqualityComparer comparer) + public WellKnownTypes(Compilation compilation, IEqualityComparer comparer) { Console = GetType("System.CommandLine.IConsole"); ParseResult = GetType("System.CommandLine.Parsing.ParseResult"); @@ -26,41 +26,47 @@ public WellKnownTypes(Compilation compilation, IEqualityComparer compilation.GetTypeByMetadataName(typeName) - ?? throw new InvalidOperationException($"Could not find well known type '{typeName}'"); + ?? throw new InvalidOperationException($"Could not find well known type '{typeName}'"); + Comparer = comparer; } - internal bool Contains(Microsoft.CodeAnalysis.ISymbol symbol) => TryGet(symbol, out _); + internal bool Contains(ISymbol symbol) => TryGet(symbol, out _); - internal bool TryGet(Microsoft.CodeAnalysis.ISymbol symbol, out Parameter? parameter) + internal bool TryGet(ISymbol symbol, out Parameter? parameter) { if (Comparer.Equals(Console, symbol)) { parameter = new ConsoleParameter(Console); return true; } + if (Comparer.Equals(InvocationContext, symbol)) { parameter = new InvocationContextParameter(InvocationContext); return true; } + if (Comparer.Equals(ParseResult, symbol)) { parameter = new ParseResultParameter(ParseResult); return true; } + if (Comparer.Equals(HelpBuilder, symbol)) { parameter = new HelpBuilderParameter(HelpBuilder); return true; } + if (Comparer.Equals(BindingContext, symbol)) { parameter = new BindingContextParameter(BindingContext); return true; } + parameter = null; return false; } } -} +} \ No newline at end of file diff --git a/src/System.CommandLine/CommandExtensions.cs b/src/System.CommandLine/CommandExtensions.cs index 74f43fc8f6..fb10d15a17 100644 --- a/src/System.CommandLine/CommandExtensions.cs +++ b/src/System.CommandLine/CommandExtensions.cs @@ -65,5 +65,16 @@ public static ParseResult Parse( string commandLine, IReadOnlyCollection? delimiters = null) => new Parser(command).Parse(commandLine); + + private const string _messageForWhenGeneratorIsNotInUse = + "This overload should not be called. You should reference the System.CommandLine.Generator package which will generate a more specific overload for your delegate."; + + public static void SetHandler( + this Command command, + TDelegate @delegate, + params ISymbol[] symbols) + { + throw new InvalidOperationException(_messageForWhenGeneratorIsNotInUse); + } } } diff --git a/src/System.CommandLine/ConsoleExtensions.cs b/src/System.CommandLine/ConsoleExtensions.cs new file mode 100644 index 0000000000..63c115cd57 --- /dev/null +++ b/src/System.CommandLine/ConsoleExtensions.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.CommandLine.IO; + +namespace System.CommandLine +{ + /// + /// Provides extension methods for . + /// + public static class ConsoleExtensions + { + public static void Write(this IConsole console, string value) => + console.Out.Write(value); + + public static void WriteLine(this IConsole console, string value) => + console.Out.WriteLine(value); + } +} \ No newline at end of file