Skip to content

Commit 8f280ec

Browse files
Keboojonsequitur
authored andcommitted
Adding support for C#10 natural type lambdas in Generator
1 parent fc76f3d commit 8f280ec

File tree

5 files changed

+203
-74
lines changed

5 files changed

+203
-74
lines changed

src/System.CommandLine.Generator.Tests/GeneratedCommandHandlerTests.cs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,100 @@ void Execute2(string value)
259259
secondValue.Should().Be("v2");
260260
}
261261

262+
[Fact]
263+
public async Task Can_generate_handler_natural_type_delegates()
264+
{
265+
string? boundName = default;
266+
int boundAge = default;
267+
IConsole? boundConsole = null;
268+
269+
void Execute(string fullnameOrNickname, IConsole console, int age)
270+
{
271+
boundName = fullnameOrNickname;
272+
boundConsole = console;
273+
boundAge = age;
274+
}
275+
276+
var nameArgument = new Argument<string>();
277+
var ageOption = new Option<int>("--age");
278+
279+
var command = new Command("command")
280+
{
281+
nameArgument,
282+
ageOption
283+
};
284+
285+
command.SetHandler(Execute, nameArgument, ageOption);
286+
287+
await command.InvokeAsync("command Gandalf --age 425", _console);
288+
289+
boundName.Should().Be("Gandalf");
290+
boundAge.Should().Be(425);
291+
boundConsole.Should().NotBeNull();
292+
}
293+
294+
[Fact]
295+
public async Task Can_generate_handler_for_lambda()
296+
{
297+
string? boundName = default;
298+
int boundAge = default;
299+
IConsole? boundConsole = null;
300+
301+
var nameArgument = new Argument<string>();
302+
var ageOption = new Option<int>("--age");
303+
304+
var command = new Command("command")
305+
{
306+
nameArgument,
307+
ageOption
308+
};
309+
310+
command.SetHandler((string fullnameOrNickname, IConsole console, int age) =>
311+
{
312+
boundName = fullnameOrNickname;
313+
boundConsole = console;
314+
boundAge = age;
315+
}, nameArgument, ageOption);
316+
317+
await command.InvokeAsync("command Gandalf --age 425", _console);
318+
319+
boundName.Should().Be("Gandalf");
320+
boundAge.Should().Be(425);
321+
boundConsole.Should().NotBeNull();
322+
}
323+
324+
[Fact]
325+
public async Task Can_generate_handler_for_lambda_wth_return_type_specified()
326+
{
327+
string? boundName = default;
328+
int boundAge = default;
329+
IConsole? boundConsole = null;
330+
331+
var nameArgument = new Argument<string>();
332+
var ageOption = new Option<int>("--age");
333+
334+
var command = new Command("command")
335+
{
336+
nameArgument,
337+
ageOption
338+
};
339+
340+
command.SetHandler(int (string fullnameOrNickname, IConsole console, int age) =>
341+
{
342+
boundName = fullnameOrNickname;
343+
boundConsole = console;
344+
boundAge = age;
345+
return 42;
346+
}, nameArgument, ageOption);
347+
348+
int rv = await command.InvokeAsync("command Gandalf --age 425", _console);
349+
350+
rv.Should().Be(42);
351+
boundName.Should().Be("Gandalf");
352+
boundAge.Should().Be(425);
353+
boundConsole.Should().NotBeNull();
354+
}
355+
262356
public class Character
263357
{
264358
public Character(string? fullName, int age)

src/System.CommandLine.Generator.Tests/System.CommandLine.Generator.Tests.csproj

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
<PropertyGroup>
33
<TargetFrameworks>net5.0</TargetFrameworks>
44
<TargetFrameworks Condition="'$(OS)' == 'Windows_NT'">$(TargetFrameworks);net462</TargetFrameworks>
5-
<LangVersion>9</LangVersion>
65
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
76
<ExcludeFromSourceBuild>true</ExcludeFromSourceBuild>
87
<Nullable>enable</Nullable>

src/System.CommandLine.Generator/CommandHandlerSourceGenerator.cs

Lines changed: 106 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) .NET Foundation and contributors. All rights reserved.
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33

4+
using System.CommandLine.Generator.Invocations;
45
using System.Linq;
56
using System.Text;
67
using Microsoft.CodeAnalysis;
@@ -45,109 +46,144 @@ internal static class GeneratedCommandHandlers
4546

4647
foreach (var invocation in rx.Invocations)
4748
{
48-
var methodParameters = invocation.Parameters
49-
.Select(x => x.GetMethodParameter())
50-
.Where(x => !string.IsNullOrWhiteSpace(x.Name))
51-
.ToArray();
52-
53-
builder.Append(
54-
@$"
55-
public static void SetHandler<{string.Join(", ", Enumerable.Range(1, invocation.NumberOfGenerericParameters).Select(x => $@"T{x}"))}>(
56-
this Command command,");
57-
builder.Append($@"
58-
{invocation.DelegateType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)} method");
49+
var methodParameters = GetMethodParameters(invocation);
5950

60-
if (methodParameters.Length > 0)
61-
{
62-
builder.Append(",");
63-
builder.AppendLine(string.Join(", ", methodParameters.Select(x => $@"
64-
{x.Type} {x.Name}")) + ")");
65-
}
66-
else
67-
{
68-
builder.Append(")");
69-
}
51+
GenerateSetHandler(builder, invocation, methodParameters, handlerCount, true);
52+
//The non-geric overload is to support C# 10 natural type lambdas
53+
GenerateSetHandler(builder, invocation, methodParameters, handlerCount, false);
7054

71-
builder.Append(@"
72-
{");
73-
builder.Append($@"
74-
command.Handler = new GeneratedHandler_{handlerCount}(method");
55+
GenerateHandlerClass(builder, invocation, methodParameters, handlerCount);
7556

76-
if (methodParameters.Length > 0)
77-
{
78-
builder.Append(", ");
79-
builder.Append(string.Join(", ", methodParameters.Select(x => x.Name)));
80-
}
57+
//TODO: fully qualify type names
58+
59+
handlerCount++;
60+
}
8161

82-
builder.Append(");");
62+
builder.Append(@"
63+
}
64+
}
65+
");
8366

84-
builder.AppendLine(@"
85-
}");
67+
context.AddSource("CommandHandlerGeneratorExtensions_Generated.g.cs", builder.ToString());
68+
}
8669

87-
//TODO: fully qualify type names
88-
builder.Append($@"
70+
private static void GenerateHandlerClass(
71+
StringBuilder builder,
72+
DelegateInvocation invocation,
73+
(string Type, string Name)[] methodParameters,
74+
int handlerCount)
75+
{
76+
builder.Append($@"
8977
private class GeneratedHandler_{handlerCount} : {ICommandHandlerType}
9078
{{
9179
public GeneratedHandler_{handlerCount}(
92-
{invocation.DelegateType} method");
80+
{invocation.DelegateType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)} method");
9381

94-
if (methodParameters.Length > 0)
95-
{
96-
builder.Append(",");
97-
builder.Append(string.Join($", ", methodParameters.Select(x => $@"
82+
if (methodParameters.Length > 0)
83+
{
84+
builder.Append(",");
85+
builder.Append(string.Join($", ", methodParameters.Select(x => $@"
9886
{x.Type} {x.Name}")) + ")");
99-
}
100-
else
101-
{
102-
builder.Append(")");
103-
}
87+
}
88+
else
89+
{
90+
builder.Append(")");
91+
}
10492

105-
builder.Append($@"
93+
builder.Append($@"
10694
{{
10795
Method = method;");
108-
foreach (var propertyAssignment in invocation.Parameters
109-
.Select(x => x.GetPropertyAssignment())
110-
.Where(x => !string.IsNullOrWhiteSpace(x)))
111-
{
112-
builder.Append($@"
96+
foreach (var propertyAssignment in invocation.Parameters
97+
.Select(x => x.GetPropertyAssignment())
98+
.Where(x => !string.IsNullOrWhiteSpace(x)))
99+
{
100+
builder.Append($@"
113101
{propertyAssignment}");
114-
}
102+
}
115103

116-
builder.AppendLine($@"
104+
builder.AppendLine($@"
117105
}}
118106
119-
public {invocation.DelegateType} Method {{ get; }}");
107+
public {invocation.DelegateType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)} Method {{ get; }}");
120108

121-
foreach (var propertyDeclaration in invocation.Parameters
122-
.Select(x => x.GetPropertyDeclaration())
123-
.Where(x => !string.IsNullOrWhiteSpace(x)))
124-
{
125-
builder.Append($@"
109+
foreach (var propertyDeclaration in invocation.Parameters
110+
.Select(x => x.GetPropertyDeclaration())
111+
.Where(x => !string.IsNullOrWhiteSpace(x)))
112+
{
113+
builder.Append($@"
126114
{propertyDeclaration}");
127-
}
115+
}
128116

129-
builder.Append($@"
130-
public async Task<int> InvokeAsync(InvocationContext context)
117+
builder.Append($@"
118+
public async global::System.Threading.Tasks.Task<int> InvokeAsync(global::System.CommandLine.Invocation.InvocationContext context)
131119
{{");
132-
builder.Append($@"
120+
builder.Append($@"
133121
{invocation.InvokeContents()}");
134-
builder.Append($@"
122+
builder.Append($@"
135123
}}
136124
}}");
137-
handlerCount++;
125+
}
126+
127+
private static (string Type, string Name)[] GetMethodParameters(DelegateInvocation invocation)
128+
{
129+
return invocation.Parameters
130+
.Select(x => x.GetMethodParameter())
131+
.Where(x => !string.IsNullOrWhiteSpace(x.Name))
132+
.ToArray();
133+
}
134+
135+
private static void GenerateSetHandler(
136+
StringBuilder builder,
137+
DelegateInvocation invocation,
138+
(string Type, string Name)[] methodParameters,
139+
int handlerCount,
140+
bool isGeneric)
141+
{
142+
builder.Append(
143+
@$"
144+
public static void SetHandler");
145+
146+
if (isGeneric)
147+
{
148+
builder.Append($"<{string.Join(", ", Enumerable.Range(1, invocation.NumberOfGenerericParameters).Select(x => $@"T{x}"))}>");
149+
}
150+
builder.Append(@$"(
151+
this global::System.CommandLine.Command command,");
152+
153+
builder.Append($@"
154+
{invocation.DelegateType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)} method");
155+
156+
if (methodParameters.Length > 0)
157+
{
158+
builder.Append(",");
159+
builder.AppendLine(string.Join(", ", methodParameters.Select(x => $@"
160+
{x.Type} {x.Name}")) + ")");
161+
}
162+
else
163+
{
164+
builder.Append(")");
138165
}
139166

140167
builder.Append(@"
141-
}
142-
}
143-
");
168+
{");
169+
builder.Append($@"
170+
command.Handler = new GeneratedHandler_{handlerCount}(method");
144171

145-
context.AddSource("CommandHandlerGeneratorExtensions_Generated.g.cs", builder.ToString());
172+
if (methodParameters.Length > 0)
173+
{
174+
builder.Append(", ");
175+
builder.Append(string.Join(", ", methodParameters.Select(x => x.Name)));
176+
}
177+
178+
builder.Append(");");
179+
180+
builder.AppendLine(@"
181+
}");
146182
}
147183

148184
public void Initialize(GeneratorInitializationContext context)
149185
{
150-
// Debugger.Launch();
186+
//System.Diagnostics.Debugger.Launch();
151187

152188
context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
153189
}

src/System.CommandLine.Generator/Invocations/ConstructorModelBindingInvocation.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,11 @@ public override string InvokeContents()
4848
{
4949
case ReturnPattern.InvocationContextExitCode:
5050
builder.Append(@"
51-
return await Task.FromResult(context.ExitCode);");
51+
return await global::System.Threading.Tasks.Task.FromResult(context.ExitCode);");
5252
break;
5353
case ReturnPattern.FunctionReturnValue:
5454
builder.Append(@"
55-
return await Task.FromResult(rv);");
55+
return await global::System.Threading.Tasks.Task.FromResult(rv);");
5656
break;
5757
case ReturnPattern.AwaitFunction:
5858
builder.Append(@"

src/System.CommandLine.Generator/System.CommandLine.Generator.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
<ItemGroup>
1313
<None Include="$(OutputPath)/$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
1414
<None Include="$(OutputPath)/../../../System.CommandLine.Generator.CommandHandler/**/System.CommandLine.Generator.CommandHandler.dll"
15-
Pack="true"
15+
Pack="true"
1616
PackagePath="lib/netstandard2.0/System.CommandLine.Generator.CommandHandler.dll" />
1717
</ItemGroup>
1818

0 commit comments

Comments
 (0)