mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-26 07:47:54 +01:00
Refactored `GenerateMethods` to replace LINQ with explicit loops, improving readability and maintainability. Added support for `notnull` and `new()` constraints and ensured constraint clauses are only added when necessary. Streamlined `switch` statements for `RefKind` modifiers and default parameter values to use more compact syntax. Updated exception message formatting for clarity. Commented out unused "Throws" method generation. Removed an extraneous closing brace for cleanup.
357 lines
16 KiB
C#
357 lines
16 KiB
C#
using System.Collections.Immutable;
|
|
using System.Text;
|
|
using Microsoft.CodeAnalysis;
|
|
using Microsoft.CodeAnalysis.CSharp;
|
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
|
|
namespace TerminalGuiFluentTestingXunit.Generator;
|
|
|
|
[Generator]
|
|
public class TheGenerator : IIncrementalGenerator
|
|
{
|
|
/// <inheritdoc/>
|
|
public void Initialize (IncrementalGeneratorInitializationContext context)
|
|
{
|
|
IncrementalValuesProvider<ClassDeclarationSyntax> provider = context.SyntaxProvider.CreateSyntaxProvider (
|
|
static (node, _) => IsClass (node, "XunitContextExtensions"),
|
|
static (ctx, _) =>
|
|
(ClassDeclarationSyntax)ctx.Node)
|
|
.Where (m => m is { });
|
|
|
|
IncrementalValueProvider<(Compilation Left, ImmutableArray<ClassDeclarationSyntax> Right)> compilation =
|
|
context.CompilationProvider.Combine (provider.Collect ());
|
|
context.RegisterSourceOutput (compilation, Execute);
|
|
}
|
|
|
|
private static bool IsClass (SyntaxNode node, string named) { return node is ClassDeclarationSyntax c && c.Identifier.Text == named; }
|
|
|
|
private void Execute (SourceProductionContext context, (Compilation Left, ImmutableArray<ClassDeclarationSyntax> Right) arg2)
|
|
{
|
|
INamedTypeSymbol assertType = arg2.Left.GetTypeByMetadataName ("Xunit.Assert")
|
|
?? throw new NotSupportedException ("Referencing codebase does not include Xunit, could not find Xunit.Assert");
|
|
|
|
GenerateMethods (assertType, context, "Equal", false);
|
|
|
|
GenerateMethods (assertType, context, "All", true);
|
|
GenerateMethods (assertType, context, "Collection", true);
|
|
GenerateMethods (assertType, context, "Contains", true);
|
|
GenerateMethods (assertType, context, "Distinct", true);
|
|
GenerateMethods (assertType, context, "DoesNotContain", true);
|
|
GenerateMethods (assertType, context, "DoesNotMatch", true);
|
|
GenerateMethods (assertType, context, "Empty", true);
|
|
GenerateMethods (assertType, context, "EndsWith", false);
|
|
GenerateMethods (assertType, context, "Equivalent", true);
|
|
GenerateMethods (assertType, context, "Fail", true);
|
|
GenerateMethods (assertType, context, "False", true);
|
|
GenerateMethods (assertType, context, "InRange", true);
|
|
GenerateMethods (assertType, context, "IsAssignableFrom", true);
|
|
GenerateMethods (assertType, context, "IsNotAssignableFrom", true);
|
|
GenerateMethods (assertType, context, "IsType", true);
|
|
GenerateMethods (assertType, context, "IsNotType", true);
|
|
|
|
GenerateMethods (assertType, context, "Matches", true);
|
|
GenerateMethods (assertType, context, "Multiple", true);
|
|
GenerateMethods (assertType, context, "NotEmpty", true);
|
|
GenerateMethods (assertType, context, "NotEqual", true);
|
|
GenerateMethods (assertType, context, "NotInRange", true);
|
|
GenerateMethods (assertType, context, "NotNull", false);
|
|
GenerateMethods (assertType, context, "NotSame", true);
|
|
GenerateMethods (assertType, context, "NotStrictEqual", true);
|
|
GenerateMethods (assertType, context, "Null", false);
|
|
GenerateMethods (assertType, context, "ProperSubset", true);
|
|
GenerateMethods (assertType, context, "ProperSuperset", true);
|
|
GenerateMethods (assertType, context, "Raises", true);
|
|
GenerateMethods (assertType, context, "RaisesAny", true);
|
|
GenerateMethods (assertType, context, "Same", true);
|
|
GenerateMethods (assertType, context, "Single", true);
|
|
GenerateMethods (assertType, context, "StartsWith", false);
|
|
|
|
GenerateMethods (assertType, context, "StrictEqual", true);
|
|
GenerateMethods (assertType, context, "Subset", true);
|
|
GenerateMethods (assertType, context, "Superset", true);
|
|
|
|
// GenerateMethods (assertType, context, "Throws", true);
|
|
// GenerateMethods (assertType, context, "ThrowsAny", true);
|
|
GenerateMethods (assertType, context, "True", false);
|
|
}
|
|
|
|
private void GenerateMethods (INamedTypeSymbol assertType, SourceProductionContext context, string methodName, bool invokeTExplicitly)
|
|
{
|
|
var sb = new StringBuilder ();
|
|
|
|
// Create a HashSet to track unique method signatures
|
|
HashSet<string> signaturesDone = new ();
|
|
|
|
List<IMethodSymbol> methods = assertType
|
|
.GetMembers (methodName)
|
|
.OfType<IMethodSymbol> ()
|
|
.ToList ();
|
|
|
|
var header = """"
|
|
#nullable enable
|
|
using TerminalGuiFluentTesting;
|
|
using Xunit;
|
|
|
|
namespace TerminalGuiFluentTestingXunit;
|
|
|
|
public static partial class XunitContextExtensions
|
|
{
|
|
|
|
|
|
"""";
|
|
|
|
var tail = """
|
|
|
|
}
|
|
""";
|
|
|
|
sb.AppendLine (header);
|
|
|
|
foreach (IMethodSymbol? m in methods)
|
|
{
|
|
string signature = GetModifiedMethodSignature (m, methodName, invokeTExplicitly, out string [] paramNames, out string typeParams);
|
|
|
|
if (!signaturesDone.Add (signature))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var method = $$"""
|
|
{{signature}}
|
|
{
|
|
try
|
|
{
|
|
Assert.{{methodName}}{{typeParams}} ({{string.Join (",", paramNames)}});
|
|
}
|
|
catch(Exception ex)
|
|
{
|
|
context.HardStop (ex);
|
|
|
|
|
|
throw;
|
|
|
|
}
|
|
|
|
return context;
|
|
}
|
|
""";
|
|
|
|
sb.AppendLine (method);
|
|
}
|
|
|
|
sb.AppendLine (tail);
|
|
|
|
context.AddSource ($"XunitContextExtensions{methodName}.g.cs", sb.ToString ());
|
|
}
|
|
|
|
private string GetModifiedMethodSignature (
|
|
IMethodSymbol methodSymbol,
|
|
string methodName,
|
|
bool invokeTExplicitly,
|
|
out string [] paramNames,
|
|
out string typeParams
|
|
)
|
|
{
|
|
typeParams = string.Empty;
|
|
|
|
// Create the "this GuiTestContext context" parameter
|
|
ParameterSyntax contextParam = SyntaxFactory.Parameter (SyntaxFactory.Identifier ("context"))
|
|
.WithType (SyntaxFactory.ParseTypeName ("GuiTestContext"))
|
|
.AddModifiers (SyntaxFactory.Token (SyntaxKind.ThisKeyword)); // Add the "this" keyword
|
|
|
|
// Extract the parameter names (expected and actual)
|
|
paramNames = new string [methodSymbol.Parameters.Length];
|
|
|
|
for (var i = 0; i < methodSymbol.Parameters.Length; i++)
|
|
{
|
|
paramNames [i] = methodSymbol.Parameters.ElementAt (i).Name;
|
|
|
|
// Check if the parameter name is a reserved keyword and prepend "@" if it is
|
|
if (IsReservedKeyword (paramNames [i]))
|
|
{
|
|
paramNames [i] = "@" + paramNames [i];
|
|
}
|
|
else
|
|
{
|
|
paramNames [i] = paramNames [i];
|
|
}
|
|
}
|
|
|
|
// Get the current method parameters and add the context parameter at the start
|
|
List<ParameterSyntax> parameters = methodSymbol.Parameters.Select (p => CreateParameter (p)).ToList ();
|
|
|
|
parameters.Insert (0, contextParam); // Insert 'context' as the first parameter
|
|
|
|
// Change the return type to GuiTestContext
|
|
TypeSyntax returnType = SyntaxFactory.ParseTypeName ("GuiTestContext");
|
|
|
|
// Change the method name to AssertEqual
|
|
SyntaxToken newMethodName = SyntaxFactory.Identifier ($"Assert{methodName}");
|
|
|
|
// Handle generic type parameters if the method is generic
|
|
TypeParameterSyntax [] typeParameters = methodSymbol.TypeParameters.Select (
|
|
tp =>
|
|
SyntaxFactory.TypeParameter (SyntaxFactory.Identifier (tp.Name))
|
|
)
|
|
.ToArray ();
|
|
|
|
MethodDeclarationSyntax dec = SyntaxFactory.MethodDeclaration (returnType, newMethodName)
|
|
.WithModifiers (
|
|
SyntaxFactory.TokenList (
|
|
SyntaxFactory.Token (SyntaxKind.PublicKeyword),
|
|
SyntaxFactory.Token (SyntaxKind.StaticKeyword)))
|
|
.WithParameterList (SyntaxFactory.ParameterList (SyntaxFactory.SeparatedList (parameters)));
|
|
|
|
if (typeParameters.Any ())
|
|
{
|
|
// Add the <T> here
|
|
dec = dec.WithTypeParameterList (SyntaxFactory.TypeParameterList (SyntaxFactory.SeparatedList (typeParameters)));
|
|
|
|
// Handle type parameter constraints
|
|
List<TypeParameterConstraintClauseSyntax> constraintClauses = new ();
|
|
|
|
foreach (ITypeParameterSymbol tp in methodSymbol.TypeParameters)
|
|
{
|
|
List<TypeParameterConstraintSyntax> constraints = new ();
|
|
|
|
// Add class/struct constraints
|
|
if (tp.HasReferenceTypeConstraint)
|
|
{
|
|
constraints.Add (SyntaxFactory.ClassOrStructConstraint (SyntaxKind.ClassConstraint));
|
|
}
|
|
else if (tp.HasValueTypeConstraint)
|
|
{
|
|
constraints.Add (SyntaxFactory.ClassOrStructConstraint (SyntaxKind.StructConstraint));
|
|
}
|
|
else if (tp.HasNotNullConstraint)
|
|
{
|
|
// Add notnull constraint
|
|
constraints.Add (SyntaxFactory.TypeConstraint (SyntaxFactory.IdentifierName ("notnull")));
|
|
}
|
|
|
|
// Add type constraints
|
|
foreach (ITypeSymbol constraintType in tp.ConstraintTypes)
|
|
{
|
|
constraints.Add (
|
|
SyntaxFactory.TypeConstraint (
|
|
SyntaxFactory.ParseTypeName (constraintType.ToDisplayString ())));
|
|
}
|
|
|
|
// Add new() constraint
|
|
if (tp.HasConstructorConstraint)
|
|
{
|
|
constraints.Add (SyntaxFactory.ConstructorConstraint ());
|
|
}
|
|
|
|
// Only add constraint clause if there are constraints
|
|
if (constraints.Any ())
|
|
{
|
|
constraintClauses.Add (
|
|
SyntaxFactory.TypeParameterConstraintClause (tp.Name)
|
|
.WithConstraints (SyntaxFactory.SeparatedList (constraints)));
|
|
}
|
|
}
|
|
|
|
if (constraintClauses.Any ())
|
|
{
|
|
dec = dec.WithConstraintClauses (SyntaxFactory.List (constraintClauses));
|
|
}
|
|
|
|
// Add the <T> here
|
|
if (invokeTExplicitly)
|
|
{
|
|
typeParams = "<" + string.Join (", ", typeParameters.Select (tp => tp.Identifier.ValueText)) + ">";
|
|
}
|
|
}
|
|
|
|
// Build the method signature syntax tree
|
|
MethodDeclarationSyntax methodSyntax = dec.NormalizeWhitespace ();
|
|
|
|
// Convert the method syntax to a string
|
|
var methodString = methodSyntax.ToString ();
|
|
|
|
return methodString;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a <see cref="ParameterSyntax"/> from a discovered parameter on real xunit method parameter
|
|
/// <paramref name="p"/>
|
|
/// </summary>
|
|
/// <param name="p"></param>
|
|
/// <returns></returns>
|
|
private ParameterSyntax CreateParameter (IParameterSymbol p)
|
|
{
|
|
string paramName = p.Name;
|
|
|
|
// Check if the parameter name is a reserved keyword and prepend "@" if it is
|
|
if (IsReservedKeyword (paramName))
|
|
{
|
|
paramName = "@" + paramName;
|
|
}
|
|
|
|
// Create the basic parameter syntax with the modified name and type
|
|
ParameterSyntax parameterSyntax = SyntaxFactory.Parameter (SyntaxFactory.Identifier (paramName))
|
|
.WithType (SyntaxFactory.ParseTypeName (p.Type.ToDisplayString ()));
|
|
|
|
// Add 'params' keyword if the parameter has the Params modifier
|
|
var modifiers = new List<SyntaxToken> ();
|
|
|
|
if (p.IsParams)
|
|
{
|
|
modifiers.Add (SyntaxFactory.Token (SyntaxKind.ParamsKeyword));
|
|
}
|
|
|
|
// Handle ref/out/in modifiers
|
|
if (p.RefKind != RefKind.None)
|
|
{
|
|
SyntaxKind modifierKind = p.RefKind switch
|
|
{
|
|
RefKind.Ref => SyntaxKind.RefKeyword,
|
|
RefKind.Out => SyntaxKind.OutKeyword,
|
|
RefKind.In => SyntaxKind.InKeyword,
|
|
_ => throw new NotSupportedException ($"Unsupported RefKind: {p.RefKind}")
|
|
};
|
|
|
|
|
|
modifiers.Add (SyntaxFactory.Token (modifierKind));
|
|
}
|
|
|
|
|
|
if (modifiers.Any ())
|
|
{
|
|
parameterSyntax = parameterSyntax.WithModifiers (SyntaxFactory.TokenList (modifiers));
|
|
}
|
|
|
|
// Add default value if one is present
|
|
if (p.HasExplicitDefaultValue)
|
|
{
|
|
ExpressionSyntax defaultValueExpression = p.ExplicitDefaultValue switch
|
|
{
|
|
null => SyntaxFactory.LiteralExpression (SyntaxKind.NullLiteralExpression),
|
|
bool b => SyntaxFactory.LiteralExpression (
|
|
b
|
|
? SyntaxKind.TrueLiteralExpression
|
|
: SyntaxKind.FalseLiteralExpression),
|
|
int i => SyntaxFactory.LiteralExpression (
|
|
SyntaxKind.NumericLiteralExpression,
|
|
SyntaxFactory.Literal (i)),
|
|
double d => SyntaxFactory.LiteralExpression (
|
|
SyntaxKind.NumericLiteralExpression,
|
|
SyntaxFactory.Literal (d)),
|
|
string s => SyntaxFactory.LiteralExpression (
|
|
SyntaxKind.StringLiteralExpression,
|
|
SyntaxFactory.Literal (s)),
|
|
_ => SyntaxFactory.ParseExpression (p.ExplicitDefaultValue.ToString ()) // Fallback
|
|
};
|
|
|
|
parameterSyntax = parameterSyntax.WithDefault (
|
|
SyntaxFactory.EqualsValueClause (defaultValueExpression)
|
|
);
|
|
}
|
|
|
|
return parameterSyntax;
|
|
}
|
|
|
|
// Helper method to check if a parameter name is a reserved keyword
|
|
private bool IsReservedKeyword (string name) { return string.Equals (name, "object"); }
|
|
} |