This commit is contained in:
Tyrrrz
2024-09-19 22:41:12 +03:00
parent 8546c54c23
commit 71fe231f28
11 changed files with 430 additions and 65 deletions

View File

@@ -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<ITypeSymbol>();
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<ITypeSymbol>();
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<CommandInputSymbol>(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<string> ValidatorTypeNames
);
private record CommandOptionSymbol(
PropertyInfo Property,
string[] Names,
string? Description,
string? ConverterTypeName,
IReadOnlyList<string> ValidatorTypeNames
);
private record CommandSymbol(
string TypeName,
string? Name,
IReadOnlyList<CommandParameterSymbol> Parameters,
IReadOnlyList<CommandOptionSymbol> Options
);
}

View File

@@ -1,4 +1,5 @@
using Microsoft.CodeAnalysis;
using CliFx.SourceGeneration.SemanticModel;
using Microsoft.CodeAnalysis;
namespace CliFx.SourceGeneration;

View File

@@ -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<TypeDescriptor> validatorTypes
)
{
public PropertyDescriptor Property { get; } = property;
public bool IsSequence { get; } = isSequence;
public string? Description { get; } = description;
public TypeDescriptor? ConverterType { get; } = converterType;
public IReadOnlyList<TypeDescriptor> ValidatorTypes { get; } = validatorTypes;
}
internal partial class CommandInputSymbol : IEquatable<CommandInputSymbol>
{
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);
}

View File

@@ -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<TypeDescriptor> 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<CommandOptionSymbol>
{
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);
}

View File

@@ -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<TypeDescriptor> 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<CommandParameterSymbol>
{
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);
}

View File

@@ -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<CommandInputSymbol> inputs
)
{
public TypeDescriptor Type { get; } = type;
public string? Name { get; } = name;
public string? Description { get; } = description;
public IReadOnlyList<CommandInputSymbol> Inputs { get; } = inputs;
public IReadOnlyList<CommandParameterSymbol> Parameters =>
Inputs.OfType<CommandParameterSymbol>().ToArray();
public IReadOnlyList<CommandOptionSymbol> Options =>
Inputs.OfType<CommandOptionSymbol>().ToArray();
}
internal partial class CommandSymbol : IEquatable<CommandSymbol>
{
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);
}

View File

@@ -1,4 +1,4 @@
namespace CliFx.SourceGeneration;
namespace CliFx.SourceGeneration.SemanticModel;
internal static class KnownSymbolNames
{

View File

@@ -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<PropertyDescriptor>
{
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);
}

View File

@@ -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<TypeDescriptor>
{
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();
}

View File

@@ -0,0 +1,9 @@
using System;
namespace CliFx.SourceGeneration.Utils.Extensions;
internal static class GenericExtensions
{
public static TOut Pipe<TIn, TOut>(this TIn input, Func<TIn, TOut> transform) =>
transform(input);
}

View File

@@ -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<T>(this IEnumerable<T> source, string separator) =>
string.Join(separator, source);
}