Skip to content

Commit db0c0a2

Browse files
committed
Optimized custom type parsing
Optimized custom type parsing for CLI options/arguments feature added in v1.5.0. It's now fully compatible (stable) with AOT compilation !
1 parent a25a3d4 commit db0c0a2

File tree

53 files changed

+1535
-262
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+1535
-262
lines changed

docs/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,7 @@ The following types for properties is supported:
192192
* `Uri`, `IPAddress`, `IPEndPoint`
193193

194194
* Any type with a public constructor or a static `Parse` method with a string parameter - These types can be bound/parsed
195-
automatically even if they are wrapped with `Enumerable` or `Nullable` type (note that as of current version, custom class
196-
support when using AOT compilation is not stable but trimming works)
195+
automatically even if they are wrapped with `Enumerable` or `Nullable` type.
197196
```c#
198197
[CliCommand]
199198
public class ArgumentConverterCliCommand
@@ -625,6 +624,7 @@ The properties for `CliArgument` attribute (see [CliArgumentAttribute](https://d
625624
- Description
626625
- HelpName
627626
- Hidden
627+
- Required
628628
- Arity
629629
- AllowedValues
630630

src/Directory.Build.props

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
<PropertyGroup>
44
<!-- https://learn.microsoft.com/en-us/dotnet/core/project-sdk/msbuild-props#generateassemblyinfo -->
5-
<VersionPrefix>1.5.2</VersionPrefix>
5+
<LangVersion>10.0</LangVersion>
6+
<VersionPrefix>1.5.4</VersionPrefix>
67
<Product>DotMake Command-Line</Product>
78
<Company>DotMake</Company>
89
<!-- Copyright is also used for NuGet metadata -->

src/DotMake.CommandLine.SourceGeneration/CliArgumentInfo.cs

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Collections.Immutable;
4+
using System.ComponentModel;
45
using System.Linq;
56
using Microsoft.CodeAnalysis;
67
using Microsoft.CodeAnalysis.CSharp;
@@ -132,7 +133,19 @@ public void AppendCSharpCreateString(CodeStringBuilder sb, string varName, strin
132133
: Symbol.Name.StripSuffixes(Suffixes).ToCase(Parent.Settings.NameCasingConvention);
133134

134135
sb.AppendLine($"// Argument for '{Symbol.Name}' property");
135-
using (sb.AppendBlockStart($"var {varName} = new {ArgumentClassNamespace}.{ArgumentClassName}<{Symbol.Type.ToReferenceString()}>(\"{argumentName}\")", ";"))
136+
using (sb.AppendParamsBlockStart($"var {varName} = new {ArgumentClassNamespace}.{ArgumentClassName}<{Symbol.Type.ToReferenceString()}>"))
137+
{
138+
sb.AppendLine($"\"{argumentName}\"");
139+
if (Converter != null)
140+
{
141+
var parseArgument = $", GetParseArgument<{Symbol.Type.ToReferenceString()}, {Converter.ContainingType.ToReferenceString()}>";
142+
if (Converter.Name == ".ctor")
143+
sb.AppendLine($"{parseArgument}(input => new {Converter.ContainingType.ToReferenceString()}(input))");
144+
else
145+
sb.AppendLine($"{parseArgument}(input => {Converter.ToReferenceString()}(input))");
146+
}
147+
}
148+
using (sb.AppendBlockStart(null, ";"))
136149
{
137150
foreach (var kvp in AttributeArguments)
138151
{
@@ -162,14 +175,6 @@ public void AppendCSharpCreateString(CodeStringBuilder sb, string varName, strin
162175

163176
if (!Required)
164177
sb.AppendLine($"{varName}.SetDefaultValue({varDefaultValue});");
165-
166-
if (Converter != null)
167-
{
168-
if (Converter.Name == ".ctor")
169-
sb.AppendLine($"RegisterArgumentConverter(input => new {Converter.ContainingType.ToReferenceString()}(input));");
170-
else
171-
sb.AppendLine($"RegisterArgumentConverter(input => {Converter.ToReferenceString()}(input));");
172-
}
173178
}
174179

175180
public bool Equals(CliArgumentInfo other)
@@ -179,14 +184,20 @@ public bool Equals(CliArgumentInfo other)
179184

180185
public static ITypeSymbol FindTypeIfNeedsConverter(ITypeSymbol type)
181186
{
182-
// note we want System.String and not string so use MetadataName instead of ToDisplayString or ToReferenceString
183-
while (!SupportedConverters.Contains(type.ToCompareString()))
187+
while (true)
184188
{
185-
var itemType = type.GetTypeIfNullable();
186-
if (itemType != null)
187-
type = itemType;
188-
189-
itemType = type.GetElementTypeIfEnumerable();
189+
// note we want System.String and not string so use MetadataName instead of ToDisplayString or ToReferenceString
190+
if (type.TypeKind == TypeKind.Enum || SupportedConverters.Contains(type.ToCompareString()))
191+
return null;
192+
193+
var underlyingType = type.GetTypeIfNullable();
194+
if (underlyingType != null)
195+
{
196+
type = underlyingType;
197+
continue;
198+
}
199+
200+
var itemType = type.GetElementTypeIfEnumerable();
190201
if (itemType != null)
191202
{
192203
type = itemType;
@@ -195,8 +206,6 @@ public static ITypeSymbol FindTypeIfNeedsConverter(ITypeSymbol type)
195206

196207
return type;
197208
}
198-
199-
return null;
200209
}
201210

202211
public static IMethodSymbol FindConverter(ITypeSymbol type)

src/DotMake.CommandLine.SourceGeneration/CliOptionInfo.cs

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,19 @@ public void AppendCSharpCreateString(CodeStringBuilder sb, string varName, strin
106106
.AddPrefix(Parent.Settings.NamePrefixConvention);
107107

108108
sb.AppendLine($"// Option for '{Symbol.Name}' property");
109-
using (sb.AppendBlockStart($"var {varName} = new {OptionClassNamespace}.{OptionClassName}<{Symbol.Type}>(\"{optionName}\")", ";"))
109+
using (sb.AppendParamsBlockStart($"var {varName} = new {OptionClassNamespace}.{OptionClassName}<{Symbol.Type.ToReferenceString()}>"))
110+
{
111+
sb.AppendLine($"\"{optionName}\"");
112+
if (Converter != null)
113+
{
114+
var parseArgument = $", GetParseArgument<{Symbol.Type.ToReferenceString()}, {Converter.ContainingType.ToReferenceString()}>";
115+
if (Converter.Name == ".ctor")
116+
sb.AppendLine($"{parseArgument}(input => new {Converter.ContainingType.ToReferenceString()}(input))");
117+
else
118+
sb.AppendLine($"{parseArgument}(input => {Converter.ToReferenceString()}(input))");
119+
}
120+
}
121+
using (sb.AppendBlockStart(null, ";"))
110122
{
111123
foreach (var kvp in AttributeArguments)
112124
{
@@ -163,14 +175,6 @@ public void AppendCSharpCreateString(CodeStringBuilder sb, string varName, strin
163175
}
164176
}
165177
}
166-
167-
if (Converter != null)
168-
{
169-
if (Converter.Name == ".ctor")
170-
sb.AppendLine($"RegisterArgumentConverter(input => new {Converter.ContainingType.ToReferenceString()}(input));");
171-
else
172-
sb.AppendLine($"RegisterArgumentConverter(input => {Converter.ToReferenceString()}(input));");
173-
}
174178
}
175179

176180
public bool Equals(CliOptionInfo other)

src/DotMake.CommandLine.SourceGeneration/CodeStringBuilder.cs

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,41 +32,53 @@ public void AppendLine(string line)
3232

3333
public void AppendLineEnd() => sb.AppendLine();
3434

35-
public IDisposable AppendBlockStart(string line = null, string afterBlock = null)
35+
public IDisposable AppendBlockStart(string line, string startBlock, string endBlock, string afterBlock)
3636
{
3737
if (line != null)
3838
AppendLine(line);
3939

40-
AppendLine("{");
40+
AppendLine(startBlock);
4141

4242
IndentLevel++;
4343

44-
return new BlockTracker(this, afterBlock);
44+
return new BlockTracker(this, endBlock, afterBlock);
45+
}
46+
47+
public IDisposable AppendBlockStart(string line = null, string afterBlock = null)
48+
{
49+
return AppendBlockStart(line, "{", "}", afterBlock);
50+
}
51+
52+
public IDisposable AppendParamsBlockStart(string line = null, string afterBlock = null)
53+
{
54+
return AppendBlockStart(line, "(", ")", afterBlock);
4555
}
4656

47-
public void AppendBlockEnd(string afterBlock = null)
57+
public void AppendBlockEnd(string endBlock, string afterBlock = null)
4858
{
4959
IndentLevel--;
5060

51-
AppendLine(string.IsNullOrEmpty(afterBlock) ? "}" : "}" + afterBlock);
61+
AppendLine(string.IsNullOrEmpty(afterBlock) ? endBlock : endBlock + afterBlock);
5262
}
5363

5464
public override string ToString() => sb.ToString();
5565

5666
private class BlockTracker : IDisposable
5767
{
5868
private readonly CodeStringBuilder parent;
69+
private readonly string endBlock;
5970
private readonly string afterBlock;
6071

61-
public BlockTracker(CodeStringBuilder parent, string afterBlock)
72+
public BlockTracker(CodeStringBuilder parent, string endBlock, string afterBlock)
6273
{
6374
this.parent = parent;
75+
this.endBlock = endBlock;
6476
this.afterBlock = afterBlock;
6577
}
6678

6779
public void Dispose()
6880
{
69-
parent.AppendBlockEnd(afterBlock);
81+
parent.AppendBlockEnd(endBlock, afterBlock);
7082
}
7183
}
7284
}

src/DotMake.CommandLine.SourceGeneration/SymbolExtensions.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ public static class SymbolExtensions
1414
memberOptions: SymbolDisplayMemberOptions.IncludeContainingType,
1515
miscellaneousOptions:
1616
SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers |
17-
SymbolDisplayMiscellaneousOptions.UseSpecialTypes |
18-
SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier);
17+
SymbolDisplayMiscellaneousOptions.UseSpecialTypes);
18+
//SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier);
1919

2020
private static readonly SymbolDisplayFormat CompareFormat =
2121
new SymbolDisplayFormat(
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
#nullable enable
4+
// ReSharper disable CheckNamespace
5+
6+
using System.Linq;
7+
8+
namespace System.CommandLine.Binding
9+
{
10+
internal sealed class ArgumentConversionResult
11+
{
12+
internal readonly Argument Argument;
13+
internal readonly object? Value;
14+
internal readonly string? ErrorMessage;
15+
internal ArgumentConversionResultType Result;
16+
17+
private ArgumentConversionResult(Argument argument, string error, ArgumentConversionResultType failure)
18+
{
19+
Argument = argument ?? throw new ArgumentNullException(nameof(argument));
20+
ErrorMessage = error ?? throw new ArgumentNullException(nameof(error));
21+
Result = failure;
22+
}
23+
24+
private ArgumentConversionResult(Argument argument, object? value)
25+
{
26+
Argument = argument ?? throw new ArgumentNullException(nameof(argument));
27+
Value = value;
28+
Result = ArgumentConversionResultType.Successful;
29+
}
30+
31+
private ArgumentConversionResult(Argument argument)
32+
{
33+
Argument = argument ?? throw new ArgumentNullException(nameof(argument));
34+
Result = ArgumentConversionResultType.NoArgument;
35+
}
36+
37+
internal ArgumentConversionResult(
38+
Argument argument,
39+
Type expectedType,
40+
string value,
41+
LocalizationResources localizationResources) :
42+
this(argument, FormatErrorMessage(argument, expectedType, value, localizationResources), ArgumentConversionResultType.FailedType)
43+
{
44+
}
45+
46+
internal static ArgumentConversionResult Failure(Argument argument, string error, ArgumentConversionResultType reason) => new(argument, error, reason);
47+
48+
public static ArgumentConversionResult Success(Argument argument, object? value) => new(argument, value);
49+
50+
internal static ArgumentConversionResult None(Argument argument) => new(argument);
51+
52+
private static string FormatErrorMessage(
53+
Argument argument,
54+
Type expectedType,
55+
string value,
56+
LocalizationResources localizationResources)
57+
{
58+
var firstParent = argument.Parents.FirstOrDefault();
59+
if (firstParent is IdentifierSymbol identifierSymbol &&
60+
firstParent.Parents.FirstOrDefault() is null)
61+
{
62+
var alias = identifierSymbol.Aliases.First();
63+
64+
switch (identifierSymbol)
65+
{
66+
case Command _:
67+
return localizationResources.ArgumentConversionCannotParseForCommand(value, alias, expectedType);
68+
case Option _:
69+
return localizationResources.ArgumentConversionCannotParseForOption(value, alias, expectedType);
70+
}
71+
}
72+
73+
return localizationResources.ArgumentConversionCannotParse(value, expectedType);
74+
}
75+
}
76+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
// ReSharper disable CheckNamespace
5+
6+
namespace System.CommandLine.Binding
7+
{
8+
internal enum ArgumentConversionResultType
9+
{
10+
NoArgument, // NoArgumentConversionResult
11+
Successful, // SuccessfulArgumentConversionResult
12+
Failed, // FailedArgumentConversionResult
13+
FailedArity, // FailedArgumentConversionArityResult
14+
FailedType, // FailedArgumentTypeConversionResult
15+
FailedTooManyArguments, // TooManyArgumentsConversionResult
16+
FailedMissingArgument, // MissingArgumentConversionResult
17+
}
18+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
#nullable enable
4+
// ReSharper disable CheckNamespace
5+
6+
using System.Collections;
7+
using System.Collections.Generic;
8+
using System.Diagnostics.CodeAnalysis;
9+
using System.Reflection;
10+
11+
namespace System.CommandLine.Binding;
12+
13+
internal static partial class ArgumentConverter
14+
{
15+
#if NET6_0_OR_GREATER
16+
private static ConstructorInfo? _listCtor;
17+
#endif
18+
19+
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050", Justification = "https://github.com/dotnet/command-line-api/issues/1638")]
20+
private static Array CreateArray(Type itemType, int capacity)
21+
=> Array.CreateInstance(itemType, capacity);
22+
23+
private static IList CreateEmptyList(Type listType)
24+
{
25+
#if NET6_0_OR_GREATER
26+
ConstructorInfo? listCtor = _listCtor;
27+
28+
if (listCtor is null)
29+
{
30+
_listCtor = listCtor = typeof(List<>).GetConstructor(Type.EmptyTypes)!;
31+
}
32+
33+
var ctor = (ConstructorInfo)listType.GetMemberWithSameMetadataDefinitionAs(listCtor);
34+
#else
35+
var ctor = listType.GetConstructor(Type.EmptyTypes);
36+
#endif
37+
38+
return (IList)ctor.Invoke(null);
39+
}
40+
41+
internal static IList CreateEnumerable(Type type, Type itemType, int capacity = 0)
42+
{
43+
if (type.IsArray)
44+
{
45+
return CreateArray(itemType, capacity);
46+
}
47+
48+
if (type.IsGenericType)
49+
{
50+
var genericTypeDefinition = type.GetGenericTypeDefinition();
51+
52+
if (genericTypeDefinition == typeof(IEnumerable<>) ||
53+
genericTypeDefinition == typeof(IList<>) ||
54+
genericTypeDefinition == typeof(ICollection<>))
55+
{
56+
return CreateArray(itemType, capacity);
57+
}
58+
59+
if (genericTypeDefinition == typeof(List<>))
60+
{
61+
return CreateEmptyList(type);
62+
}
63+
}
64+
65+
throw new ArgumentException($"Type {type} cannot be created without a custom binder.");
66+
}
67+
}

0 commit comments

Comments
 (0)