From 71fe231f2869bcb0c03dab81ad52d08faf61a8a8 Mon Sep 17 00:00:00 2001 From: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> Date: Thu, 19 Sep 2024 22:41:12 +0300 Subject: [PATCH] asdad --- .../CommandSchemaGenerator.cs | 158 +++++++++++------- .../DiagnosticDescriptors.cs | 3 +- .../SemanticModel/CommandInputSymbol.cs | 56 +++++++ .../SemanticModel/CommandOptionSymbol.cs | 57 +++++++ .../SemanticModel/CommandParameterSymbol.cs | 53 ++++++ .../SemanticModel/CommandSymbol.cs | 57 +++++++ .../{ => SemanticModel}/KnownSymbolNames.cs | 2 +- .../SemanticModel/PropertyDescriptor.cs | 37 ++++ .../SemanticModel/TypeDescriptor.cs | 40 +++++ .../Utils/Extensions/GenericExtensions.cs | 9 + .../Utils/Extensions/StringExtensions.cs | 23 ++- 11 files changed, 430 insertions(+), 65 deletions(-) create mode 100644 CliFx.SourceGeneration/SemanticModel/CommandInputSymbol.cs create mode 100644 CliFx.SourceGeneration/SemanticModel/CommandOptionSymbol.cs create mode 100644 CliFx.SourceGeneration/SemanticModel/CommandParameterSymbol.cs create mode 100644 CliFx.SourceGeneration/SemanticModel/CommandSymbol.cs rename CliFx.SourceGeneration/{ => SemanticModel}/KnownSymbolNames.cs (88%) create mode 100644 CliFx.SourceGeneration/SemanticModel/PropertyDescriptor.cs create mode 100644 CliFx.SourceGeneration/SemanticModel/TypeDescriptor.cs create mode 100644 CliFx.SourceGeneration/Utils/Extensions/GenericExtensions.cs diff --git a/CliFx.SourceGeneration/CommandSchemaGenerator.cs b/CliFx.SourceGeneration/CommandSchemaGenerator.cs index ddb28be..7d9d5b3 100644 --- a/CliFx.SourceGeneration/CommandSchemaGenerator.cs +++ b/CliFx.SourceGeneration/CommandSchemaGenerator.cs @@ -1,6 +1,6 @@ using System; -using System.Collections.Generic; using System.Linq; +using CliFx.SourceGeneration.SemanticModel; using CliFx.SourceGeneration.Utils.Extensions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -9,7 +9,7 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; namespace CliFx.SourceGeneration; [Generator] -public partial class CommandSchemaGenerator : IIncrementalGenerator +public class CommandSchemaGenerator : IIncrementalGenerator { public void Initialize(IncrementalGeneratorInitializationContext context) { @@ -69,6 +69,13 @@ public partial class CommandSchemaGenerator : IIncrementalGenerator var commandName = commandAttribute.ConstructorArguments.FirstOrDefault().Value as string; + var commandDescription = + commandAttribute + .NamedArguments.FirstOrDefault(a => + string.Equals(a.Key, "Description", StringComparison.Ordinal) + ) + .Value.Value as string; + // Get all parameter inputs var parameterSymbols = namedTypeSymbol .GetMembers() @@ -85,8 +92,9 @@ public partial class CommandSchemaGenerator : IIncrementalGenerator if (parameterAttribute is null) return null; - var order = - parameterAttribute.ConstructorArguments.FirstOrDefault().Value as int?; + var isSequence = false; // TODO + + var order = parameterAttribute.ConstructorArguments.First().Value as int?; var isRequired = parameterAttribute @@ -94,7 +102,7 @@ public partial class CommandSchemaGenerator : IIncrementalGenerator string.Equals(a.Key, "IsRequired", StringComparison.Ordinal) ) .Value.Value as bool? - ?? p.IsRequired; + ?? true; var name = parameterAttribute @@ -124,19 +132,25 @@ public partial class CommandSchemaGenerator : IIncrementalGenerator .Value.Values.CastArray(); return new CommandParameterSymbol( - new PropertyInfo( - p.Name, - p.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) + new PropertyDescriptor( + new TypeDescriptor( + p.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) + ), + p.Name ), + isSequence, order, isRequired, name, description, - converter?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + converter + ?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) + ?.Pipe(n => new TypeDescriptor(n)), validators .Select(v => v.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) ) + .Select(n => new TypeDescriptor(n)) .ToArray() ); }) @@ -159,8 +173,41 @@ public partial class CommandSchemaGenerator : IIncrementalGenerator if (optionAttribute is null) return null; - var names = - optionAttribute.ConstructorArguments.FirstOrDefault().Value as string[]; + 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 @@ -183,17 +230,26 @@ public partial class CommandSchemaGenerator : IIncrementalGenerator .Value.Values.CastArray(); return new CommandOptionSymbol( - new PropertyInfo( - p.Name, - p.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) + new PropertyDescriptor( + new TypeDescriptor( + p.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) + ), + p.Name ), - names, + isSequence, + name, + shortName, + environmentVariable, + isRequired, description, - converter?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + converter + ?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) + ?.Pipe(n => new TypeDescriptor(n)), validators .Select(v => v.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) ) + .Select(n => new TypeDescriptor(n)) .ToArray() ); }) @@ -202,10 +258,14 @@ public partial class CommandSchemaGenerator : IIncrementalGenerator return ( new CommandSymbol( - namedTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + new TypeDescriptor( + namedTypeSymbol.ToDisplayString( + SymbolDisplayFormat.FullyQualifiedFormat + ) + ), commandName, - parameterSymbols, - optionSymbols + commandDescription, + parameterSymbols.Concat(optionSymbols).ToArray() ), null ); @@ -229,33 +289,38 @@ public partial class CommandSchemaGenerator : IIncrementalGenerator using CliFx.Schema; using CliFx.Extensibility; - partial class {{ c.TypeName }} + namespace {{ c.Type.Namespace }}; + + partial class {{ c.Type.Name }} { - public static CommandSchema<{{ c.TypeName }}> Schema { get; } = new( + public static CommandSchema<{{ c.Type.FullyQualifiedName }}> Schema { get; } = new( {{ c.Name }}, + {{ c.Description }}, [ - {{ c.Parameters.Select(p => + {{ c.Inputs.Select(i => i switch { + CommandParameterSymbol parameter => // lang=csharp $$""" - new CommandParameterSchema<{{ c.TypeName }}, {{ p.Property.TypeName }}>( - new PropertyBinding<{{ c.TypeName }}, {{ p.Property.TypeName }}>( - obj => obj.{{ p.Property.Name }}, - (obj, value) => obj.{{ p.Property.Name }} = value + 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 {{ p.ConverterTypeName }}(), + new {{ i.ConverterType.FullyQualifiedName }}(), [ - {{ p.ValidatorTypeNames.Select(v => + {{ i.ValidatorTypes.Select(v => // lang=csharp - $"new {v}()").JoinToString(",\n") + $"new {v.FullyQualifiedName}()").JoinToString(",\n") }} ] ) - """ - ).JoinToString(",\n") + """, + CommandOptionSymbol option => "" + }).JoinToString(",\n") }} ] } @@ -266,34 +331,3 @@ public partial class CommandSchemaGenerator : IIncrementalGenerator ); } } - -public partial class CommandSchemaGenerator -{ - // TODO make all types structurally equatable - private record PropertyInfo(string Name, string TypeName); - - private record CommandParameterSymbol( - PropertyInfo Property, - int? Order, - bool IsRequired, - string? Name, - string? Description, - string? ConverterTypeName, - IReadOnlyList ValidatorTypeNames - ); - - private record CommandOptionSymbol( - PropertyInfo Property, - string[] Names, - string? Description, - string? ConverterTypeName, - IReadOnlyList ValidatorTypeNames - ); - - private record CommandSymbol( - string TypeName, - string? Name, - IReadOnlyList Parameters, - IReadOnlyList Options - ); -} diff --git a/CliFx.SourceGeneration/DiagnosticDescriptors.cs b/CliFx.SourceGeneration/DiagnosticDescriptors.cs index 023be11..2adbfb8 100644 --- a/CliFx.SourceGeneration/DiagnosticDescriptors.cs +++ b/CliFx.SourceGeneration/DiagnosticDescriptors.cs @@ -1,4 +1,5 @@ -using Microsoft.CodeAnalysis; +using CliFx.SourceGeneration.SemanticModel; +using Microsoft.CodeAnalysis; namespace CliFx.SourceGeneration; diff --git a/CliFx.SourceGeneration/SemanticModel/CommandInputSymbol.cs b/CliFx.SourceGeneration/SemanticModel/CommandInputSymbol.cs new file mode 100644 index 0000000..3ea3c59 --- /dev/null +++ b/CliFx.SourceGeneration/SemanticModel/CommandInputSymbol.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace CliFx.SourceGeneration.SemanticModel; + +internal abstract partial class CommandInputSymbol( + PropertyDescriptor property, + bool isSequence, + string? description, + TypeDescriptor? converterType, + IReadOnlyList validatorTypes +) +{ + public PropertyDescriptor Property { get; } = property; + + public bool IsSequence { get; } = isSequence; + + public string? Description { get; } = description; + + public TypeDescriptor? ConverterType { get; } = converterType; + + public IReadOnlyList ValidatorTypes { get; } = validatorTypes; +} + +internal partial class CommandInputSymbol : IEquatable +{ + public bool Equals(CommandInputSymbol? other) + { + if (ReferenceEquals(null, other)) + return false; + if (ReferenceEquals(this, other)) + return true; + + return Property.Equals(other.Property) + && IsSequence == other.IsSequence + && Description == other.Description + && Equals(ConverterType, other.ConverterType) + && ValidatorTypes.SequenceEqual(other.ValidatorTypes); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + return false; + if (ReferenceEquals(this, obj)) + return true; + if (obj.GetType() != GetType()) + return false; + + return Equals((CommandInputSymbol)obj); + } + + public override int GetHashCode() => + HashCode.Combine(Property, IsSequence, Description, ConverterType, ValidatorTypes); +} diff --git a/CliFx.SourceGeneration/SemanticModel/CommandOptionSymbol.cs b/CliFx.SourceGeneration/SemanticModel/CommandOptionSymbol.cs new file mode 100644 index 0000000..5bf038f --- /dev/null +++ b/CliFx.SourceGeneration/SemanticModel/CommandOptionSymbol.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; + +namespace CliFx.SourceGeneration.SemanticModel; + +internal partial class CommandOptionSymbol( + PropertyDescriptor property, + bool isSequence, + string? name, + char? shortName, + string? environmentVariable, + bool isRequired, + string? description, + TypeDescriptor? converterType, + IReadOnlyList validatorTypes +) : CommandInputSymbol(property, isSequence, description, converterType, validatorTypes) +{ + public string? Name { get; } = name; + + public char? ShortName { get; } = shortName; + + public string? EnvironmentVariable { get; } = environmentVariable; + + public bool IsRequired { get; } = isRequired; +} + +internal partial class CommandOptionSymbol : IEquatable +{ + public bool Equals(CommandOptionSymbol? other) + { + if (ReferenceEquals(null, other)) + return false; + if (ReferenceEquals(this, other)) + return true; + + return base.Equals(other) + && Name == other.Name + && ShortName == other.ShortName + && EnvironmentVariable == other.EnvironmentVariable + && IsRequired == other.IsRequired; + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + return false; + if (ReferenceEquals(this, obj)) + return true; + if (obj.GetType() != GetType()) + return false; + + return Equals((CommandOptionSymbol)obj); + } + + public override int GetHashCode() => + HashCode.Combine(base.GetHashCode(), Name, ShortName, EnvironmentVariable, IsRequired); +} diff --git a/CliFx.SourceGeneration/SemanticModel/CommandParameterSymbol.cs b/CliFx.SourceGeneration/SemanticModel/CommandParameterSymbol.cs new file mode 100644 index 0000000..81471a6 --- /dev/null +++ b/CliFx.SourceGeneration/SemanticModel/CommandParameterSymbol.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; + +namespace CliFx.SourceGeneration.SemanticModel; + +internal partial class CommandParameterSymbol( + PropertyDescriptor property, + bool isSequence, + int order, + string name, + bool isRequired, + string? description, + TypeDescriptor? converterType, + IReadOnlyList validatorTypes +) : CommandInputSymbol(property, isSequence, description, converterType, validatorTypes) +{ + public int Order { get; } = order; + + public string Name { get; } = name; + + public bool IsRequired { get; } = isRequired; +} + +internal partial class CommandParameterSymbol : IEquatable +{ + public bool Equals(CommandParameterSymbol? other) + { + if (ReferenceEquals(null, other)) + return false; + if (ReferenceEquals(this, other)) + return true; + + return base.Equals(other) + && Order == other.Order + && Name == other.Name + && IsRequired == other.IsRequired; + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + return false; + if (ReferenceEquals(this, obj)) + return true; + if (obj.GetType() != GetType()) + return false; + + return Equals((CommandParameterSymbol)obj); + } + + public override int GetHashCode() => + HashCode.Combine(base.GetHashCode(), Order, Name, IsRequired); +} diff --git a/CliFx.SourceGeneration/SemanticModel/CommandSymbol.cs b/CliFx.SourceGeneration/SemanticModel/CommandSymbol.cs new file mode 100644 index 0000000..aa174f2 --- /dev/null +++ b/CliFx.SourceGeneration/SemanticModel/CommandSymbol.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace CliFx.SourceGeneration.SemanticModel; + +internal partial class CommandSymbol( + TypeDescriptor type, + string? name, + string? description, + IReadOnlyList inputs +) +{ + public TypeDescriptor Type { get; } = type; + + public string? Name { get; } = name; + + public string? Description { get; } = description; + + public IReadOnlyList Inputs { get; } = inputs; + + public IReadOnlyList Parameters => + Inputs.OfType().ToArray(); + + public IReadOnlyList Options => + Inputs.OfType().ToArray(); +} + +internal partial class CommandSymbol : IEquatable +{ + public bool Equals(CommandSymbol? other) + { + if (ReferenceEquals(null, other)) + return false; + if (ReferenceEquals(this, other)) + return true; + + return Type.Equals(other.Type) + && Name == other.Name + && Description == other.Description + && Inputs.SequenceEqual(other.Inputs); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + return false; + if (ReferenceEquals(this, obj)) + return true; + if (obj.GetType() != GetType()) + return false; + + return Equals((CommandSymbol)obj); + } + + public override int GetHashCode() => HashCode.Combine(Type, Name, Description, Inputs); +} diff --git a/CliFx.SourceGeneration/KnownSymbolNames.cs b/CliFx.SourceGeneration/SemanticModel/KnownSymbolNames.cs similarity index 88% rename from CliFx.SourceGeneration/KnownSymbolNames.cs rename to CliFx.SourceGeneration/SemanticModel/KnownSymbolNames.cs index e08fda9..4fbbc16 100644 --- a/CliFx.SourceGeneration/KnownSymbolNames.cs +++ b/CliFx.SourceGeneration/SemanticModel/KnownSymbolNames.cs @@ -1,4 +1,4 @@ -namespace CliFx.SourceGeneration; +namespace CliFx.SourceGeneration.SemanticModel; internal static class KnownSymbolNames { diff --git a/CliFx.SourceGeneration/SemanticModel/PropertyDescriptor.cs b/CliFx.SourceGeneration/SemanticModel/PropertyDescriptor.cs new file mode 100644 index 0000000..05278ed --- /dev/null +++ b/CliFx.SourceGeneration/SemanticModel/PropertyDescriptor.cs @@ -0,0 +1,37 @@ +using System; + +namespace CliFx.SourceGeneration.SemanticModel; + +internal partial class PropertyDescriptor(TypeDescriptor type, string name) +{ + public TypeDescriptor Type { get; } = type; + + public string Name { get; } = name; +} + +internal partial class PropertyDescriptor : IEquatable +{ + public bool Equals(PropertyDescriptor? other) + { + if (ReferenceEquals(null, other)) + return false; + if (ReferenceEquals(this, other)) + return true; + + return Type.Equals(other.Type) && Name == other.Name; + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + return false; + if (ReferenceEquals(this, obj)) + return true; + if (obj.GetType() != GetType()) + return false; + + return Equals((PropertyDescriptor)obj); + } + + public override int GetHashCode() => HashCode.Combine(Type, Name); +} diff --git a/CliFx.SourceGeneration/SemanticModel/TypeDescriptor.cs b/CliFx.SourceGeneration/SemanticModel/TypeDescriptor.cs new file mode 100644 index 0000000..1fa61a5 --- /dev/null +++ b/CliFx.SourceGeneration/SemanticModel/TypeDescriptor.cs @@ -0,0 +1,40 @@ +using System; +using CliFx.SourceGeneration.Utils.Extensions; + +namespace CliFx.SourceGeneration.SemanticModel; + +internal partial class TypeDescriptor(string fullyQualifiedName) +{ + public string FullyQualifiedName { get; } = fullyQualifiedName; + + public string Namespace { get; } = fullyQualifiedName.SubstringUntilLast("."); + + public string Name { get; } = fullyQualifiedName.SubstringAfterLast("."); +} + +internal partial class TypeDescriptor : IEquatable +{ + public bool Equals(TypeDescriptor? other) + { + if (ReferenceEquals(null, other)) + return false; + if (ReferenceEquals(this, other)) + return true; + + return FullyQualifiedName == other.FullyQualifiedName; + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + return false; + if (ReferenceEquals(this, obj)) + return true; + if (obj.GetType() != GetType()) + return false; + + return Equals((TypeDescriptor)obj); + } + + public override int GetHashCode() => FullyQualifiedName.GetHashCode(); +} diff --git a/CliFx.SourceGeneration/Utils/Extensions/GenericExtensions.cs b/CliFx.SourceGeneration/Utils/Extensions/GenericExtensions.cs new file mode 100644 index 0000000..d810221 --- /dev/null +++ b/CliFx.SourceGeneration/Utils/Extensions/GenericExtensions.cs @@ -0,0 +1,9 @@ +using System; + +namespace CliFx.SourceGeneration.Utils.Extensions; + +internal static class GenericExtensions +{ + public static TOut Pipe(this TIn input, Func transform) => + transform(input); +} diff --git a/CliFx.SourceGeneration/Utils/Extensions/StringExtensions.cs b/CliFx.SourceGeneration/Utils/Extensions/StringExtensions.cs index 33dc7a0..d8b513a 100644 --- a/CliFx.SourceGeneration/Utils/Extensions/StringExtensions.cs +++ b/CliFx.SourceGeneration/Utils/Extensions/StringExtensions.cs @@ -1,9 +1,30 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace CliFx.SourceGeneration.Utils.Extensions; internal static class StringExtensions { + public static string SubstringUntilLast( + this string str, + string sub, + StringComparison comparison = StringComparison.Ordinal + ) + { + var index = str.LastIndexOf(sub, comparison); + return index < 0 ? str : str[..index]; + } + + public static string SubstringAfterLast( + this string str, + string sub, + StringComparison comparison = StringComparison.Ordinal + ) + { + var index = str.LastIndexOf(sub, comparison); + return index >= 0 ? str.Substring(index + sub.Length, str.Length - index - sub.Length) : ""; + } + public static string JoinToString(this IEnumerable source, string separator) => string.Join(separator, source); }