mirror of
https://github.com/Tyrrrz/CliFx.git
synced 2025-10-25 15:19:17 +00:00
wip as fuck
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
<GenerateDependencyFile>true</GenerateDependencyFile>
|
||||
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
|
||||
<NoWarn>$(NoWarn);RS1025;RS1026</NoWarn>
|
||||
<NoWarn>$(NoWarn);RS1035</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
@@ -19,8 +19,8 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CSharpier.MsBuild" Version="0.28.2" PrivateAssets="all" />
|
||||
<!-- Make sure to target the lowest possible version of the compiler for wider support -->
|
||||
<PackageReference Include="Microsoft.CodeAnalysis" Version="3.0.0" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.0.0" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis" Version="4.11.0" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" PrivateAssets="all" />
|
||||
<PackageReference Include="PolyShim" Version="1.12.0" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
299
CliFx.SourceGeneration/CommandSchemaGenerator.cs
Normal file
299
CliFx.SourceGeneration/CommandSchemaGenerator.cs
Normal file
@@ -0,0 +1,299 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CliFx.SourceGeneration.Utils.Extensions;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
||||
namespace CliFx.SourceGeneration;
|
||||
|
||||
[Generator]
|
||||
public partial class CommandSchemaGenerator : IIncrementalGenerator
|
||||
{
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
var values = context.SyntaxProvider.ForAttributeWithMetadataName<(
|
||||
CommandSymbol?,
|
||||
Diagnostic?
|
||||
)>(
|
||||
KnownSymbolNames.CliFxCommandAttribute,
|
||||
(n, _) => n is TypeDeclarationSyntax,
|
||||
(x, _) =>
|
||||
{
|
||||
// Predicate ensures that these casts are safe
|
||||
var typeDeclarationSyntax = (TypeDeclarationSyntax)x.TargetNode;
|
||||
var namedTypeSymbol = (INamedTypeSymbol)x.TargetSymbol;
|
||||
|
||||
// Check if the target type and all its containing types are partial
|
||||
if (
|
||||
typeDeclarationSyntax
|
||||
.AncestorsAndSelf()
|
||||
.Any(a =>
|
||||
a is TypeDeclarationSyntax t
|
||||
&& !t.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword))
|
||||
)
|
||||
)
|
||||
{
|
||||
return (
|
||||
null,
|
||||
Diagnostic.Create(
|
||||
DiagnosticDescriptors.CommandMustBePartial,
|
||||
typeDeclarationSyntax.Identifier.GetLocation()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Check if the target type implements ICommand
|
||||
var hasCommandInterface = namedTypeSymbol.AllInterfaces.Any(i =>
|
||||
i.DisplayNameMatches("CliFx.ICommand")
|
||||
);
|
||||
|
||||
if (!hasCommandInterface)
|
||||
{
|
||||
return (
|
||||
null,
|
||||
Diagnostic.Create(
|
||||
DiagnosticDescriptors.CommandMustImplementInterface,
|
||||
namedTypeSymbol.Locations.First()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Get the command name
|
||||
var commandAttribute = x.Attributes.First(a =>
|
||||
a.AttributeClass?.DisplayNameMatches(KnownSymbolNames.CliFxCommandAttribute)
|
||||
== true
|
||||
);
|
||||
|
||||
var commandName =
|
||||
commandAttribute.ConstructorArguments.FirstOrDefault().Value as string;
|
||||
|
||||
// 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 order =
|
||||
parameterAttribute.ConstructorArguments.FirstOrDefault().Value as int?;
|
||||
|
||||
var isRequired =
|
||||
parameterAttribute
|
||||
.NamedArguments.FirstOrDefault(a =>
|
||||
string.Equals(a.Key, "IsRequired", StringComparison.Ordinal)
|
||||
)
|
||||
.Value.Value as bool?
|
||||
?? p.IsRequired;
|
||||
|
||||
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 PropertyInfo(
|
||||
p.Name,
|
||||
p.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
|
||||
),
|
||||
order,
|
||||
isRequired,
|
||||
name,
|
||||
description,
|
||||
converter?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
|
||||
validators
|
||||
.Select(v =>
|
||||
v.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
|
||||
)
|
||||
.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 names =
|
||||
optionAttribute.ConstructorArguments.FirstOrDefault().Value as string[];
|
||||
|
||||
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 PropertyInfo(
|
||||
p.Name,
|
||||
p.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
|
||||
),
|
||||
names,
|
||||
description,
|
||||
converter?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
|
||||
validators
|
||||
.Select(v =>
|
||||
v.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
|
||||
)
|
||||
.ToArray()
|
||||
);
|
||||
})
|
||||
.WhereNotNull()
|
||||
.ToArray();
|
||||
|
||||
return (
|
||||
new CommandSymbol(
|
||||
namedTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
|
||||
commandName,
|
||||
parameterSymbols,
|
||||
optionSymbols
|
||||
),
|
||||
null
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// Report diagnostics
|
||||
var diagnostics = values.Select((v, _) => v.Item2).WhereNotNull();
|
||||
context.RegisterSourceOutput(diagnostics, (x, d) => x.ReportDiagnostic(d));
|
||||
|
||||
// Generate source
|
||||
var symbols = values.Select((v, _) => v.Item1).WhereNotNull();
|
||||
context.RegisterSourceOutput(
|
||||
symbols,
|
||||
(x, c) =>
|
||||
{
|
||||
var source =
|
||||
// lang=csharp
|
||||
$$"""
|
||||
using System.Linq;
|
||||
using CliFx.Schema;
|
||||
using CliFx.Extensibility;
|
||||
|
||||
partial class {{ c.TypeName }}
|
||||
{
|
||||
public static CommandSchema<{{ c.TypeName }}> Schema { get; } = new(
|
||||
{{ c.Name }},
|
||||
[
|
||||
{{ c.Parameters.Select(p =>
|
||||
// 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
|
||||
),
|
||||
p.Order,
|
||||
p.IsRequired,
|
||||
p.Name,
|
||||
p.Description,
|
||||
new {{ p.ConverterTypeName }}(),
|
||||
[
|
||||
{{ p.ValidatorTypeNames.Select(v =>
|
||||
// lang=csharp
|
||||
$"new {v}()").JoinToString(",\n")
|
||||
}}
|
||||
]
|
||||
)
|
||||
"""
|
||||
).JoinToString(",\n")
|
||||
}}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
x.AddSource($"{c.TypeName}.CommandSchema.Generated.cs", source);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
26
CliFx.SourceGeneration/DiagnosticDescriptors.cs
Normal file
26
CliFx.SourceGeneration/DiagnosticDescriptors.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace CliFx.SourceGeneration;
|
||||
|
||||
internal static class DiagnosticDescriptors
|
||||
{
|
||||
public static DiagnosticDescriptor CommandMustBePartial { get; } =
|
||||
new(
|
||||
$"{nameof(CliFx)}_{nameof(CommandMustBePartial)}",
|
||||
"Command types must be declared as `partial`",
|
||||
"This type (and all its containing types, if present) must be declared as `partial` in order to be a valid command.",
|
||||
"CliFx",
|
||||
DiagnosticSeverity.Error,
|
||||
true
|
||||
);
|
||||
|
||||
public static DiagnosticDescriptor CommandMustImplementInterface { get; } =
|
||||
new(
|
||||
$"{nameof(CliFx)}_{nameof(CommandMustImplementInterface)}",
|
||||
$"Commands must implement the `{KnownSymbolNames.CliFxCommandInterface}` interface",
|
||||
$"This type must implement the `{KnownSymbolNames.CliFxCommandInterface}` interface in order to be a valid command.",
|
||||
"CliFx",
|
||||
DiagnosticSeverity.Error,
|
||||
true
|
||||
);
|
||||
}
|
||||
10
CliFx.SourceGeneration/KnownSymbolNames.cs
Normal file
10
CliFx.SourceGeneration/KnownSymbolNames.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace CliFx.SourceGeneration;
|
||||
|
||||
internal static class KnownSymbolNames
|
||||
{
|
||||
public const string CliFxCommandInterface = "CliFx.ICommand";
|
||||
public const string CliFxCommandAttribute = "CliFx.Attributes.CommandAttribute";
|
||||
public const string CliFxCommandParameterAttribute =
|
||||
"CliFx.Attributes.CommandParameterAttribute";
|
||||
public const string CliFxCommandOptionAttribute = "CliFx.Attributes.CommandOptionAttribute";
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CliFx.SourceGeneration.Utils.Extensions;
|
||||
|
||||
internal static class CollectionExtensions
|
||||
{
|
||||
public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> source)
|
||||
where T : class
|
||||
{
|
||||
foreach (var i in source)
|
||||
{
|
||||
if (i is not null)
|
||||
yield return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
20
CliFx.SourceGeneration/Utils/Extensions/RoslynExtensions.cs
Normal file
20
CliFx.SourceGeneration/Utils/Extensions/RoslynExtensions.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace CliFx.SourceGeneration.Utils.Extensions;
|
||||
|
||||
internal static class RoslynExtensions
|
||||
{
|
||||
public static bool DisplayNameMatches(this ISymbol symbol, string name) =>
|
||||
string.Equals(
|
||||
// Fully qualified name, without `global::`
|
||||
symbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat),
|
||||
name,
|
||||
StringComparison.Ordinal
|
||||
);
|
||||
|
||||
public static IncrementalValuesProvider<T> WhereNotNull<T>(
|
||||
this IncrementalValuesProvider<T?> values
|
||||
)
|
||||
where T : class => values.Where(i => i is not null);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CliFx.SourceGeneration.Utils.Extensions;
|
||||
|
||||
internal static class StringExtensions
|
||||
{
|
||||
public static string JoinToString<T>(this IEnumerable<T> source, string separator) =>
|
||||
string.Join(separator, source);
|
||||
}
|
||||
@@ -4,7 +4,6 @@ using System.Globalization;
|
||||
using System.Linq;
|
||||
using CliFx.Infrastructure;
|
||||
using CliFx.Schema;
|
||||
using CliFx.Utils.Extensions;
|
||||
|
||||
namespace CliFx.Formatting;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user