This commit is contained in:
Tyrrrz
2024-09-21 21:03:05 +03:00
parent 71fe231f28
commit 40beb283d5
12 changed files with 270 additions and 280 deletions

View File

@@ -1,5 +1,4 @@
using System;
using System.Linq;
using System.Linq;
using CliFx.SourceGeneration.SemanticModel;
using CliFx.SourceGeneration.Utils.Extensions;
using Microsoft.CodeAnalysis;
@@ -21,13 +20,13 @@ public class CommandSchemaGenerator : IIncrementalGenerator
(n, _) => n is TypeDeclarationSyntax,
(x, _) =>
{
// Predicate ensures that these casts are safe
var typeDeclarationSyntax = (TypeDeclarationSyntax)x.TargetNode;
var namedTypeSymbol = (INamedTypeSymbol)x.TargetSymbol;
// Predicate above ensures that these casts are safe
var commandTypeSyntax = (TypeDeclarationSyntax)x.TargetNode;
var commandTypeSymbol = (INamedTypeSymbol)x.TargetSymbol;
// Check if the target type and all its containing types are partial
if (
typeDeclarationSyntax
commandTypeSyntax
.AncestorsAndSelf()
.Any(a =>
a is TypeDeclarationSyntax t
@@ -39,14 +38,14 @@ public class CommandSchemaGenerator : IIncrementalGenerator
null,
Diagnostic.Create(
DiagnosticDescriptors.CommandMustBePartial,
typeDeclarationSyntax.Identifier.GetLocation()
commandTypeSyntax.Identifier.GetLocation()
)
);
}
// Check if the target type implements ICommand
var hasCommandInterface = namedTypeSymbol.AllInterfaces.Any(i =>
i.DisplayNameMatches("CliFx.ICommand")
var hasCommandInterface = commandTypeSymbol.AllInterfaces.Any(i =>
i.DisplayNameMatches(KnownSymbolNames.CliFxCommandInterface)
);
if (!hasCommandInterface)
@@ -55,220 +54,22 @@ public class CommandSchemaGenerator : IIncrementalGenerator
null,
Diagnostic.Create(
DiagnosticDescriptors.CommandMustImplementInterface,
namedTypeSymbol.Locations.First()
commandTypeSymbol.Locations.First()
)
);
}
// Get the command name
// Resolve the command
var commandAttribute = x.Attributes.First(a =>
a.AttributeClass?.DisplayNameMatches(KnownSymbolNames.CliFxCommandAttribute)
== true
);
var commandName =
commandAttribute.ConstructorArguments.FirstOrDefault().Value as string;
var command = CommandSymbol.FromSymbol(commandTypeSymbol, commandAttribute);
var commandDescription =
commandAttribute
.NamedArguments.FirstOrDefault(a =>
string.Equals(a.Key, "Description", StringComparison.Ordinal)
)
.Value.Value as string;
// TODO: validate command
// Get all parameter inputs
var parameterSymbols = namedTypeSymbol
.GetMembers()
.OfType<IPropertySymbol>()
.Select(p =>
{
var parameterAttribute = p.GetAttributes()
.FirstOrDefault(a =>
a.AttributeClass?.DisplayNameMatches(
KnownSymbolNames.CliFxCommandParameterAttribute
) == true
);
if (parameterAttribute is null)
return null;
var isSequence = false; // TODO
var order = parameterAttribute.ConstructorArguments.First().Value as int?;
var isRequired =
parameterAttribute
.NamedArguments.FirstOrDefault(a =>
string.Equals(a.Key, "IsRequired", StringComparison.Ordinal)
)
.Value.Value as bool?
?? true;
var name =
parameterAttribute
.NamedArguments.FirstOrDefault(a =>
string.Equals(a.Key, "Name", StringComparison.Ordinal)
)
.Value.Value as string;
var description =
parameterAttribute
.NamedArguments.FirstOrDefault(a =>
string.Equals(a.Key, "Description", StringComparison.Ordinal)
)
.Value.Value as string;
var converter =
parameterAttribute
.NamedArguments.FirstOrDefault(a =>
string.Equals(a.Key, "Converter", StringComparison.Ordinal)
)
.Value.Value as ITypeSymbol;
var validators = parameterAttribute
.NamedArguments.FirstOrDefault(a =>
string.Equals(a.Key, "Validators", StringComparison.Ordinal)
)
.Value.Values.CastArray<ITypeSymbol>();
return new CommandParameterSymbol(
new PropertyDescriptor(
new TypeDescriptor(
p.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
),
p.Name
),
isSequence,
order,
isRequired,
name,
description,
converter
?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
?.Pipe(n => new TypeDescriptor(n)),
validators
.Select(v =>
v.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
)
.Select(n => new TypeDescriptor(n))
.ToArray()
);
})
.WhereNotNull()
.ToArray();
// Get all option inputs
var optionSymbols = namedTypeSymbol
.GetMembers()
.OfType<IPropertySymbol>()
.Select(p =>
{
var optionAttribute = p.GetAttributes()
.FirstOrDefault(a =>
a.AttributeClass?.DisplayNameMatches(
KnownSymbolNames.CliFxCommandOptionAttribute
) == true
);
if (optionAttribute is null)
return null;
var isSequence = false; // TODO
var name =
optionAttribute
.ConstructorArguments.Where(a =>
a.Type?.SpecialType == SpecialType.System_String
)
.Select(a => a.Value)
.FirstOrDefault() as string;
var shortName =
optionAttribute
.ConstructorArguments.Where(a =>
a.Type?.SpecialType == SpecialType.System_Char
)
.Select(a => a.Value)
.FirstOrDefault() as char?;
var environmentVariable =
optionAttribute
.NamedArguments.FirstOrDefault(a =>
string.Equals(
a.Key,
"EnvironmentVariable",
StringComparison.Ordinal
)
)
.Value.Value as string;
var isRequired =
optionAttribute
.NamedArguments.Where(a => a.Key == "IsRequired")
.Select(a => a.Value.Value)
.FirstOrDefault() as bool?
?? p.IsRequired;
var description =
optionAttribute
.NamedArguments.FirstOrDefault(a =>
string.Equals(a.Key, "Description", StringComparison.Ordinal)
)
.Value.Value as string;
var converter =
optionAttribute
.NamedArguments.FirstOrDefault(a =>
string.Equals(a.Key, "Converter", StringComparison.Ordinal)
)
.Value.Value as ITypeSymbol;
var validators = optionAttribute
.NamedArguments.FirstOrDefault(a =>
string.Equals(a.Key, "Validators", StringComparison.Ordinal)
)
.Value.Values.CastArray<ITypeSymbol>();
return new CommandOptionSymbol(
new PropertyDescriptor(
new TypeDescriptor(
p.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
),
p.Name
),
isSequence,
name,
shortName,
environmentVariable,
isRequired,
description,
converter
?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
?.Pipe(n => new TypeDescriptor(n)),
validators
.Select(v =>
v.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
)
.Select(n => new TypeDescriptor(n))
.ToArray()
);
})
.WhereNotNull()
.ToArray();
return (
new CommandSymbol(
new TypeDescriptor(
namedTypeSymbol.ToDisplayString(
SymbolDisplayFormat.FullyQualifiedFormat
)
),
commandName,
commandDescription,
parameterSymbols.Concat<CommandInputSymbol>(optionSymbols).ToArray()
),
null
);
return (command, null);
}
);
@@ -276,58 +77,54 @@ public class CommandSchemaGenerator : IIncrementalGenerator
var diagnostics = values.Select((v, _) => v.Item2).WhereNotNull();
context.RegisterSourceOutput(diagnostics, (x, d) => x.ReportDiagnostic(d));
// Generate source
// Generate command schemas
var symbols = values.Select((v, _) => v.Item1).WhereNotNull();
context.RegisterSourceOutput(
symbols,
(x, c) =>
{
var source =
x.AddSource(
$"{c.Type.FullyQualifiedName}.CommandSchema.Generated.cs",
// lang=csharp
$$"""
using System.Linq;
using CliFx.Schema;
using CliFx.Extensibility;
namespace {{ c.Type.Namespace }};
partial class {{ c.Type.Name }}
{
public static CommandSchema<{{ c.Type.FullyQualifiedName }}> Schema { get; } = new(
{{ c.Name }},
{{ c.Description }},
[
{{ c.Inputs.Select(i => i switch {
CommandParameterSymbol parameter =>
// lang=csharp
$$"""
new CommandParameterSchema<{{ c.Type.FullyQualifiedName }}, {{ i.Property.Type.FullyQualifiedName }}>(
new PropertyBinding<{{ c.Type.FullyQualifiedName }}, {{ i.Property.Type.FullyQualifiedName }}>(
obj => obj.{{ i.Property.Name }},
(obj, value) => obj.{{ i.Property.Name }} = value
),
p.Order,
p.IsRequired,
p.Name,
p.Description,
new {{ i.ConverterType.FullyQualifiedName }}(),
[
{{ i.ValidatorTypes.Select(v =>
// lang=csharp
$"new {v.FullyQualifiedName}()").JoinToString(",\n")
}}
]
)
""",
CommandOptionSymbol option => ""
}).JoinToString(",\n")
}}
]
}
""";
namespace {{c.Type.Namespace}};
x.AddSource($"{c.TypeName}.CommandSchema.Generated.cs", source);
}
partial class {{c.Type.Name}}
{
public static CliFx.Schema.CommandSchema<{{c.Type.FullyQualifiedName}}> Schema { get; } = {{c.GenerateSchemaInitializationCode()}};
}
"""
)
);
// Generate extension methods
var symbolsCollected = symbols.Collect();
context.RegisterSourceOutput(
symbolsCollected,
(x, cs) =>
x.AddSource(
"CommandSchemaExtensions.Generated.cs",
// lang=csharp
$$"""
namespace CliFx;
static partial class GeneratedExtensions
{
public static CliFx.CliApplicationBuilder AddCommandsFromThisAssembly(this CliFx.CliApplicationBuilder builder)
{
{{
cs.Select(c => c.Type.FullyQualifiedName)
.Select(t =>
// lang=csharp
$"builder.AddCommand({t}.Schema);"
)
.JoinToString("\n")
}}
return builder;
}
}
"""
)
);
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis;
namespace CliFx.SourceGeneration.SemanticModel;
@@ -54,3 +55,12 @@ internal partial class CommandInputSymbol : IEquatable<CommandInputSymbol>
public override int GetHashCode() =>
HashCode.Combine(Property, IsSequence, Description, ConverterType, ValidatorTypes);
}
internal partial class CommandInputSymbol
{
public static bool IsSequenceType(ITypeSymbol type) =>
type.AllInterfaces.Any(i =>
i.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T
)
&& type.SpecialType != SpecialType.System_String;
}

View File

@@ -1,5 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using CliFx.SourceGeneration.Utils.Extensions;
using Microsoft.CodeAnalysis;
namespace CliFx.SourceGeneration.SemanticModel;
@@ -55,3 +58,33 @@ internal partial class CommandOptionSymbol : IEquatable<CommandOptionSymbol>
public override int GetHashCode() =>
HashCode.Combine(base.GetHashCode(), Name, ShortName, EnvironmentVariable, IsRequired);
}
internal partial class CommandOptionSymbol
{
public static CommandOptionSymbol FromSymbol(
IPropertySymbol property,
AttributeData attribute
) =>
new(
PropertyDescriptor.FromSymbol(property),
IsSequenceType(property.Type),
attribute
.ConstructorArguments.FirstOrDefault(a =>
a.Type?.SpecialType == SpecialType.System_String
)
.Value as string,
attribute
.ConstructorArguments.FirstOrDefault(a =>
a.Type?.SpecialType == SpecialType.System_Char
)
.Value as char?,
attribute.GetNamedArgumentValue("EnvironmentVariable", default(string)),
attribute.GetNamedArgumentValue("IsRequired", property.IsRequired),
attribute.GetNamedArgumentValue("Description", default(string)),
TypeDescriptor.FromSymbol(attribute.GetNamedArgumentValue<ITypeSymbol?>("Converter")),
attribute
.GetNamedArgumentValues<ITypeSymbol>("Validators")
.Select(TypeDescriptor.FromSymbol)
.ToArray()
);
}

View File

@@ -1,5 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using CliFx.SourceGeneration.Utils.Extensions;
using Microsoft.CodeAnalysis;
namespace CliFx.SourceGeneration.SemanticModel;
@@ -51,3 +54,24 @@ internal partial class CommandParameterSymbol : IEquatable<CommandParameterSymbo
public override int GetHashCode() =>
HashCode.Combine(base.GetHashCode(), Order, Name, IsRequired);
}
internal partial class CommandParameterSymbol
{
public static CommandParameterSymbol FromSymbol(
IPropertySymbol property,
AttributeData attribute
) =>
new(
PropertyDescriptor.FromSymbol(property),
IsSequenceType(property.Type),
(int)attribute.ConstructorArguments.First().Value!,
attribute.GetNamedArgumentValue("Name", default(string)),
attribute.GetNamedArgumentValue("IsRequired", true),
attribute.GetNamedArgumentValue("Description", default(string)),
TypeDescriptor.FromSymbol(attribute.GetNamedArgumentValue<ITypeSymbol>("Converter")),
attribute
.GetNamedArgumentValues<ITypeSymbol>("Validators")
.Select(TypeDescriptor.FromSymbol)
.ToArray()
);
}

View File

@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using CliFx.SourceGeneration.Utils.Extensions;
using Microsoft.CodeAnalysis;
namespace CliFx.SourceGeneration.SemanticModel;
@@ -24,6 +26,73 @@ internal partial class CommandSymbol(
public IReadOnlyList<CommandOptionSymbol> Options =>
Inputs.OfType<CommandOptionSymbol>().ToArray();
private string GeneratePropertyBindingInitializationCode(PropertyDescriptor property) =>
// lang=csharp
$$"""
new CliFx.Schema.PropertyBinding<{{Type.FullyQualifiedName}}, {{property
.Type
.FullyQualifiedName}}>(
(obj) => obj.{{property.Name}},
(obj, value) => obj.{{property.Name}} = value
)
""";
private string GenerateSchemaInitializationCode(CommandInputSymbol input) =>
input switch
{
CommandParameterSymbol parameter
=>
// lang=csharp
$$"""
new CliFx.Schema.CommandParameterSchema<{{Type.FullyQualifiedName}}, {{parameter
.Property
.Type
.FullyQualifiedName}}>(
{{GeneratePropertyBindingInitializationCode(parameter.Property)}},
{{parameter.IsSequence}},
{{parameter.Order}},
"{{parameter.Name}}",
{{parameter.IsRequired}},
"{{parameter.Description}}",
// TODO,
// TODO
);
""",
CommandOptionSymbol option
=>
// lang=csharp
$$"""
new CliFx.Schema.CommandOptionSchema<{{Type.FullyQualifiedName}}, {{option
.Property
.Type
.FullyQualifiedName}}>(
{{GeneratePropertyBindingInitializationCode(option.Property)}},
{{option.IsSequence}},
"{{option.Name}}",
'{{option.ShortName}}',
"{{option.EnvironmentVariable}}",
{{option.IsRequired}},
"{{option.Description}}",
// TODO,
// TODO
);
""",
_ => throw new ArgumentOutOfRangeException(nameof(input), input, null)
};
public string GenerateSchemaInitializationCode() =>
// lang=csharp
$$"""
new CliFx.Schema.CommandSchema<{{Type.FullyQualifiedName}}>(
"{{Name}}",
"{{Description}}",
new CliFx.Schema.CommandInputSchema[]
{
{{Inputs.Select(GenerateSchemaInitializationCode).JoinToString(",\n")}}
}
)
""";
}
internal partial class CommandSymbol : IEquatable<CommandSymbol>
@@ -55,3 +124,44 @@ internal partial class CommandSymbol : IEquatable<CommandSymbol>
public override int GetHashCode() => HashCode.Combine(Type, Name, Description, Inputs);
}
internal partial class CommandSymbol
{
public static CommandSymbol FromSymbol(INamedTypeSymbol symbol, AttributeData attribute)
{
var inputs = new List<CommandInputSymbol>();
foreach (var property in symbol.GetMembers().OfType<IPropertySymbol>())
{
var parameterAttribute = property
.GetAttributes()
.FirstOrDefault(a =>
a.AttributeClass?.Name == KnownSymbolNames.CliFxCommandParameterAttribute
);
if (parameterAttribute is not null)
{
inputs.Add(CommandParameterSymbol.FromSymbol(property, parameterAttribute));
continue;
}
var optionAttribute = property
.GetAttributes()
.FirstOrDefault(a =>
a.AttributeClass?.Name == KnownSymbolNames.CliFxCommandOptionAttribute
);
if (optionAttribute is not null)
{
inputs.Add(CommandOptionSymbol.FromSymbol(property, optionAttribute));
continue;
}
}
return new CommandSymbol(
TypeDescriptor.FromSymbol(symbol),
attribute.ConstructorArguments.FirstOrDefault().Value as string,
attribute.GetNamedArgumentValue("Description", default(string)),
inputs
);
}
}

View File

@@ -1,4 +1,5 @@
using System;
using Microsoft.CodeAnalysis;
namespace CliFx.SourceGeneration.SemanticModel;
@@ -35,3 +36,9 @@ internal partial class PropertyDescriptor : IEquatable<PropertyDescriptor>
public override int GetHashCode() => HashCode.Combine(Type, Name);
}
internal partial class PropertyDescriptor
{
public static PropertyDescriptor FromSymbol(IPropertySymbol symbol) =>
new(TypeDescriptor.FromSymbol(symbol.Type), symbol.Name);
}

View File

@@ -1,5 +1,6 @@
using System;
using CliFx.SourceGeneration.Utils.Extensions;
using Microsoft.CodeAnalysis;
namespace CliFx.SourceGeneration.SemanticModel;
@@ -38,3 +39,9 @@ internal partial class TypeDescriptor : IEquatable<TypeDescriptor>
public override int GetHashCode() => FullyQualifiedName.GetHashCode();
}
internal partial class TypeDescriptor
{
public static TypeDescriptor FromSymbol(ITypeSymbol symbol) =>
new(symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
}

View File

@@ -1,4 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
namespace CliFx.SourceGeneration.Utils.Extensions;
@@ -13,6 +16,22 @@ internal static class RoslynExtensions
StringComparison.Ordinal
);
public static T GetNamedArgumentValue<T>(
this AttributeData attribute,
string name,
T defaultValue = default
) =>
attribute.NamedArguments.FirstOrDefault(i => i.Key == name).Value.Value is T valueAsT
? valueAsT
: defaultValue;
public static IReadOnlyList<T> GetNamedArgumentValues<T>(
this AttributeData attribute,
string name
)
where T : class =>
attribute.NamedArguments.FirstOrDefault(i => i.Key == name).Value.Values.CastArray<T>();
public static IncrementalValuesProvider<T> WhereNotNull<T>(
this IncrementalValuesProvider<T?> values
)

View File

@@ -125,10 +125,7 @@ public abstract class CommandInputSchema(
/// </remarks>
public abstract class CommandInputSchema<
TCommand,
[DynamicallyAccessedMembers(
DynamicallyAccessedMemberTypes.PublicMethods
)]
TProperty
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] TProperty
>(
PropertyBinding<TCommand, TProperty> property,
bool isSequence,

View File

@@ -63,10 +63,7 @@ public class CommandOptionSchema(
/// </remarks>
public class CommandOptionSchema<
TCommand,
[DynamicallyAccessedMembers(
DynamicallyAccessedMemberTypes.PublicMethods
)]
TProperty
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] TProperty
>(
PropertyBinding<TCommand, TProperty> property,
bool isSequence,

View File

@@ -42,10 +42,7 @@ public class CommandParameterSchema(
/// </remarks>
public class CommandParameterSchema<
TCommand,
[DynamicallyAccessedMembers(
DynamicallyAccessedMemberTypes.PublicMethods
)]
TProperty
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] TProperty
>(
PropertyBinding<TCommand, TProperty> property,
bool isSequence,

View File

@@ -9,10 +9,7 @@ namespace CliFx.Schema;
/// Provides read and write access to a CLR property.
/// </summary>
public class PropertyBinding(
[DynamicallyAccessedMembers(
DynamicallyAccessedMemberTypes.PublicMethods
)]
Type type,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type,
Func<object, object?> getValue,
Action<object, object?> setValue
)
@@ -20,9 +17,7 @@ public class PropertyBinding(
/// <summary>
/// Underlying CLR type of the property.
/// </summary>
[DynamicallyAccessedMembers(
DynamicallyAccessedMemberTypes.PublicMethods
)]
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)]
public Type Type { get; } = type;
/// <summary>
@@ -63,10 +58,7 @@ public class PropertyBinding(
/// </remarks>
public class PropertyBinding<
TObject,
[DynamicallyAccessedMembers(
DynamicallyAccessedMemberTypes.PublicMethods
)]
TProperty
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] TProperty
>(Func<TObject, TProperty?> getValue, Action<TObject, TProperty?> setValue)
: PropertyBinding(
typeof(TProperty),