Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 20 additions & 26 deletions Source/aweXpect.SourceGenerators/ExpectationGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Collections.Immutable;
using System.Text;
using aweXpect.SourceGenerators.Helpers;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
Expand All @@ -12,12 +13,21 @@ namespace aweXpect.SourceGenerators;
[Generator]
public class ExpectationGenerator : IIncrementalGenerator
{
private static readonly string[] _supportedAttributes =
[
nameof(SourceGenerationHelper.CreateExpectationOnAttribute),
nameof(SourceGenerationHelper.CreateExpectationOnNullableAttribute),
];

void IIncrementalGenerator.Initialize(IncrementalGeneratorInitializationContext context)
{
// Add the marker attribute to the compilation
// Add the marker attributes to the compilation
context.RegisterPostInitializationOutput(ctx => ctx.AddSource(
"CreateExpectationOnAttribute.g.cs",
SourceText.From(SourceGenerationHelper.CreateExpectationOnAttribute, Encoding.UTF8)));
context.RegisterPostInitializationOutput(ctx => ctx.AddSource(
"CreateExpectationOnNullableAttribute.g.cs",
SourceText.From(SourceGenerationHelper.CreateExpectationOnNullableAttribute, Encoding.UTF8)));

HashSet<string> files = new();
IncrementalValuesProvider<ExpectationToGenerate> expectationsToGenerate = context.SyntaxProvider
Expand Down Expand Up @@ -50,7 +60,7 @@ private static IEnumerable<ExpectationToGenerate> GetSemanticTargetForGeneration
{
INamedTypeSymbol? attributeClass = attributeData.AttributeClass;
if (attributeClass == null || !attributeClass.IsGenericType ||
attributeClass.Name != "CreateExpectationOnAttribute")
!_supportedAttributes.Contains(attributeClass.Name))
{
continue;
}
Expand All @@ -62,8 +72,7 @@ private static IEnumerable<ExpectationToGenerate> GetSemanticTargetForGeneration
continue;
}

ExpectationToGenerate? expectationToGenerate = GetExpectationToGenerate(classSymbol,
attributeData);
ExpectationToGenerate? expectationToGenerate = GetExpectationToGenerate(classSymbol, attributeData);
if (expectationToGenerate != null &&
files.Add(expectationToGenerate.Value.FileName))
{
Expand Down Expand Up @@ -112,27 +121,12 @@ private static void Execute(ExpectationToGenerate expectationToGenerate, SourceP
return null;
}

string expectationText = outcomeMethod;
string? remarks = null;
string[] usings = [];
foreach (KeyValuePair<string, TypedConstant> namedArgument in attributeData.NamedArguments)
{
switch (namedArgument.Key)
{
case "ExpectationText":
expectationText = namedArgument.Value.Value?.ToString() ?? expectationText;
break;
case "Remarks":
remarks = namedArgument.Value.Value?.ToString();
break;
case "Using":
usings =
namedArgument.Value.Values.Select(x => x.Value?.ToString()).Where(x => x != null).ToArray()!;
break;
}
}

return new ExpectationToGenerate(containingNamespace, classSymbol.Name, targetType, name, outcomeMethod,
expectationText, usings, remarks);
return new ExpectationToGenerate(
containingNamespace,
classSymbol.Name,
targetType,
name,
outcomeMethod,
attributeData);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using Microsoft.CodeAnalysis;

namespace aweXpect.SourceGenerators;
namespace aweXpect.SourceGenerators.Helpers;

internal readonly record struct ExpectationToGenerate
{
Expand All @@ -9,31 +9,54 @@ public ExpectationToGenerate(string @namespace,
INamedTypeSymbol targetType,
string name,
string outcomeMethod,
string expectationText,
string[] usings,
string? remarks)
AttributeData attributeData)
{
Namespace = @namespace;
ClassName = className;
TargetType = targetType;
TargetType = targetType.ToDisplayString();
Name = name.Replace("{Not}", "");
NegatedName = name.Replace("{Not}", "Not");
IncludeNegated = name.Contains("{Not}");
OutcomeMethod = outcomeMethod;

string expectationText = outcomeMethod;
foreach (KeyValuePair<string, TypedConstant> namedArgument in attributeData.NamedArguments)
{
switch (namedArgument.Key)
{
case "ExpectationText":
expectationText = namedArgument.Value.Value?.ToString() ?? expectationText;
break;
case "Remarks":
Remarks = namedArgument.Value.Value?.ToString();
break;
case "Using":
Usings =
namedArgument.Value.Values.Select(x => x.Value?.ToString()).Where(x => x != null).ToArray()!;
break;
}
}

IsNullable = attributeData.AttributeClass!.Name ==
nameof(SourceGenerationHelper.CreateExpectationOnNullableAttribute);
if (IsNullable)
{
TargetType += "?";
}

ExpectationText = expectationText.Replace("{not}", "").Replace(" ", " ");
NegatedExpectationText = expectationText.Replace("{not}", " not ").Replace(" ", " ");
Remarks = remarks;
Usings = usings;
FileName = $"{ClassName}.{Name}.g.cs";
}

public string[] Usings { get; }
public string[] Usings { get; } = [];
public string FileName { get; }
public bool IncludeNegated { get; }
public bool IsNullable { get; }
public string NegatedName { get; }
public string Namespace { get; }
public string ClassName { get; }
public INamedTypeSymbol TargetType { get; }
public string TargetType { get; }
public string Name { get; }
public string OutcomeMethod { get; }
public string ExpectationText { get; }
Expand Down
175 changes: 175 additions & 0 deletions Source/aweXpect.SourceGenerators/Helpers/SourceGenerationHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
namespace aweXpect.SourceGenerators.Helpers;

internal static class SourceGenerationHelper
{
public const string CreateExpectationOnAttribute =
"""
using System;

namespace aweXpect.SourceGenerators;

#nullable enable
/// <summary>
/// Create an assertion on the <typeparamref name="TTarget"/> attribute.
/// </summary>
/// <typeparam name="TTarget">The target type for the assertion</typeparam>
[System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple = true)]
internal class CreateExpectationOnAttribute<TTarget> : System.Attribute
{
public CreateExpectationOnAttribute(string methodName, string name)
{
TargetType = typeof(TTarget);
MethodName = methodName;
Name = name;
}

public Type TargetType { get; }
public string MethodName { get; }
public string Name { get; set; }
public string? ExpectationText { get; set; }
public string? Remarks { get; set; }
public string[] Using { get; set; } = [];
}
#nullable disable
""";

public const string CreateExpectationOnNullableAttribute =
"""
using System;

namespace aweXpect.SourceGenerators;

#nullable enable
/// <summary>
/// Create an assertion on the <typeparamref name="TTarget"/> attribute.
/// </summary>
/// <typeparam name="TTarget">The target type for the assertion</typeparam>
[System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple = true)]
internal class CreateExpectationOnNullableAttribute<TTarget> : System.Attribute
{
public CreateExpectationOnNullableAttribute(string methodName, string name)
{
TargetType = typeof(TTarget);
MethodName = methodName;
Name = name;
}

public Type TargetType { get; }
public string MethodName { get; }
public string Name { get; set; }
public string? ExpectationText { get; set; }
public string? Remarks { get; set; }
public string[] Using { get; set; } = [];
}
#nullable disable
""";

public static string GenerateExtensionClass(ExpectationToGenerate expectationToGenerate)
{
string result = $$"""
{{string.Join("\n", expectationToGenerate.Usings.Select(x => $"using {x};"))}}
using aweXpect.Core;
using aweXpect.Core.Constraints;
using aweXpect.Helpers;
using aweXpect.Results;

namespace {{expectationToGenerate.Namespace}};

#nullable enable
public static partial class {{expectationToGenerate.ClassName}}
{
/// <summary>
/// Verifies that the subject {{expectationToGenerate.ExpectationText}}.
/// </summary>{{expectationToGenerate.AppendRemarks()}}
public static AndOrResult<{{expectationToGenerate.TargetType}}, IThat<{{expectationToGenerate.TargetType}}>> {{expectationToGenerate.Name}}(this IThat<{{expectationToGenerate.TargetType}}> source)
=> new(source.Get().ExpectationBuilder.AddConstraint((it, grammars) =>
new {{expectationToGenerate.Name}}Constraint(it, grammars)),
source);


""";
if (expectationToGenerate.IncludeNegated)
{
result += $$"""
/// <summary>
/// Verifies that the subject {{expectationToGenerate.NegatedExpectationText}}.
/// </summary>{{expectationToGenerate.AppendRemarks()}}
public static AndOrResult<{{expectationToGenerate.TargetType}}, IThat<{{expectationToGenerate.TargetType}}>> {{expectationToGenerate.NegatedName}}(this IThat<{{expectationToGenerate.TargetType}}> source)
=> new(source.Get().ExpectationBuilder.AddConstraint((it, grammars) =>
new {{expectationToGenerate.Name}}Constraint(it, grammars).Invert()),
source);


""";
}

if (expectationToGenerate.IsNullable)
{
result += $$"""
private sealed class {{expectationToGenerate.Name}}Constraint(string it, ExpectationGrammars grammars)
: ConstraintResult.WithNotNullValue<{{expectationToGenerate.TargetType}}>(it, grammars),
IValueConstraint<{{expectationToGenerate.TargetType}}>
{
public ConstraintResult IsMetBy({{expectationToGenerate.TargetType}} actual)
{
Actual = actual;
Outcome = actual is not null && {{expectationToGenerate.OutcomeMethod.Replace("{value}", "actual.Value")}} ? Outcome.Success : Outcome.Failure;
return this;
}

protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null)
=> stringBuilder.Append("{{expectationToGenerate.ExpectationText}}");

protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null)
{
stringBuilder.Append(It).Append(" was ");
Formatter.Format(stringBuilder, Actual);
}

protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null)
=> stringBuilder.Append("{{expectationToGenerate.NegatedExpectationText}}");

protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null)
=> AppendNormalResult(stringBuilder, indentation);
}
""";
}
else
{
result += $$"""
private sealed class {{expectationToGenerate.Name}}Constraint(string it, ExpectationGrammars grammars)
: ConstraintResult.WithValue<{{expectationToGenerate.TargetType}}>(grammars),
IValueConstraint<{{expectationToGenerate.TargetType}}>
{
public ConstraintResult IsMetBy({{expectationToGenerate.TargetType}} actual)
{
Actual = actual;
Outcome = {{expectationToGenerate.OutcomeMethod.Replace("{value}", "actual")}} ? Outcome.Success : Outcome.Failure;
return this;
}

protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null)
=> stringBuilder.Append("{{expectationToGenerate.ExpectationText}}");

protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null)
{
stringBuilder.Append(it).Append(" was ");
Formatter.Format(stringBuilder, Actual);
}

protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null)
=> stringBuilder.Append("{{expectationToGenerate.NegatedExpectationText}}");

protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null)
=> AppendNormalResult(stringBuilder, indentation);
}
""";
}

result += """
}
#nullable disable
""";
return result.TrimStart();
}
}
Loading
Loading