From 011766c1e924a1f4d08e013181ab6672b6123fb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sun, 14 Sep 2025 06:32:53 +0200 Subject: [PATCH 1/3] feat: add negated nullable char expectations --- .../ExpectationGenerator.cs | 33 ++--- .../ExpectationToGenerate.cs | 39 +++-- .../SourceGenerationHelper.cs | 132 ++++++++++++----- .../That/Chars/ThatNullableChar.IsALetter.cs | 50 ++----- .../That/Chars/ThatNullableChar.IsANumber.cs | 50 ++----- .../Chars/ThatNullableChar.IsAnAsciiLetter.cs | 59 ++------ .../Chars/ThatNullableChar.IsWhiteSpace.cs | 50 ++----- .../Expected/aweXpect_net8.0.txt | 6 +- .../Expected/aweXpect_netstandard2.0.txt | 6 +- .../ThatChar.Nullable.IsNotALetter.Tests.cs | 122 ++++++++++++++++ .../ThatChar.Nullable.IsNotANumber.Tests.cs | 128 +++++++++++++++++ ...tChar.Nullable.IsNotAnAsciiLetter.Tests.cs | 122 ++++++++++++++++ ...ThatChar.Nullable.IsNotWhiteSpace.Tests.cs | 134 ++++++++++++++++++ 13 files changed, 697 insertions(+), 234 deletions(-) create mode 100644 Tests/aweXpect.Tests/Chars/ThatChar.Nullable.IsNotALetter.Tests.cs create mode 100644 Tests/aweXpect.Tests/Chars/ThatChar.Nullable.IsNotANumber.Tests.cs create mode 100644 Tests/aweXpect.Tests/Chars/ThatChar.Nullable.IsNotAnAsciiLetter.Tests.cs create mode 100644 Tests/aweXpect.Tests/Chars/ThatChar.Nullable.IsNotWhiteSpace.Tests.cs diff --git a/Source/aweXpect.SourceGenerators/ExpectationGenerator.cs b/Source/aweXpect.SourceGenerators/ExpectationGenerator.cs index d210c1d39..f57d2d109 100644 --- a/Source/aweXpect.SourceGenerators/ExpectationGenerator.cs +++ b/Source/aweXpect.SourceGenerators/ExpectationGenerator.cs @@ -12,12 +12,20 @@ namespace aweXpect.SourceGenerators; [Generator] public class ExpectationGenerator : IIncrementalGenerator { + private static 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 files = new(); IncrementalValuesProvider expectationsToGenerate = context.SyntaxProvider @@ -50,7 +58,7 @@ private static IEnumerable GetSemanticTargetForGeneration { INamedTypeSymbol? attributeClass = attributeData.AttributeClass; if (attributeClass == null || !attributeClass.IsGenericType || - attributeClass.Name != "CreateExpectationOnAttribute") + !_supportedAttributes.Contains(attributeClass.Name)) { continue; } @@ -112,27 +120,8 @@ private static void Execute(ExpectationToGenerate expectationToGenerate, SourceP return null; } - string expectationText = outcomeMethod; - string? remarks = null; - string[] usings = []; - foreach (KeyValuePair 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); + attributeData); } } diff --git a/Source/aweXpect.SourceGenerators/ExpectationToGenerate.cs b/Source/aweXpect.SourceGenerators/ExpectationToGenerate.cs index 2a97abcf7..624b38a25 100644 --- a/Source/aweXpect.SourceGenerators/ExpectationToGenerate.cs +++ b/Source/aweXpect.SourceGenerators/ExpectationToGenerate.cs @@ -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 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; } diff --git a/Source/aweXpect.SourceGenerators/SourceGenerationHelper.cs b/Source/aweXpect.SourceGenerators/SourceGenerationHelper.cs index 8659a9bc6..330aa0031 100644 --- a/Source/aweXpect.SourceGenerators/SourceGenerationHelper.cs +++ b/Source/aweXpect.SourceGenerators/SourceGenerationHelper.cs @@ -33,6 +33,37 @@ public CreateExpectationOnAttribute(string methodName, string name) #nullable disable """; + public const string CreateExpectationOnNullableAttribute = + """ + using System; + + namespace aweXpect.SourceGenerators; + + #nullable enable + /// + /// Create an assertion on the attribute. + /// + /// The target type for the assertion + [System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple = true)] + internal class CreateExpectationOnNullableAttribute : 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 = $$""" @@ -50,7 +81,7 @@ public static partial class {{expectationToGenerate.ClassName}} /// /// Verifies that the subject {{expectationToGenerate.ExpectationText}}. /// {{expectationToGenerate.AppendRemarks()}} - public static AndOrResult<{{expectationToGenerate.TargetType.ToDisplayString()}}, IThat<{{expectationToGenerate.TargetType.ToDisplayString()}}>> {{expectationToGenerate.Name}}(this IThat<{{expectationToGenerate.TargetType.ToDisplayString()}}> source) + 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); @@ -63,7 +94,7 @@ public static partial class {{expectationToGenerate.ClassName}} /// /// Verifies that the subject {{expectationToGenerate.NegatedExpectationText}}. /// {{expectationToGenerate.AppendRemarks()}} - public static AndOrResult<{{expectationToGenerate.TargetType.ToDisplayString()}}, IThat<{{expectationToGenerate.TargetType.ToDisplayString()}}>> {{expectationToGenerate.NegatedName}}(this IThat<{{expectationToGenerate.TargetType.ToDisplayString()}}> source) + 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); @@ -72,36 +103,73 @@ public static partial class {{expectationToGenerate.ClassName}} """; } - result += $$""" - private sealed class {{expectationToGenerate.Name}}Constraint(string it, ExpectationGrammars grammars) - : ConstraintResult.WithValue<{{expectationToGenerate.TargetType.ToDisplayString()}}>(grammars), - IValueConstraint<{{expectationToGenerate.TargetType.ToDisplayString()}}> - { - public ConstraintResult IsMetBy({{expectationToGenerate.TargetType.ToDisplayString()}} 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); - } - } - #nullable disable - """; + 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(); } } diff --git a/Source/aweXpect/That/Chars/ThatNullableChar.IsALetter.cs b/Source/aweXpect/That/Chars/ThatNullableChar.IsALetter.cs index 9bea2dc5c..2a477bd4f 100644 --- a/Source/aweXpect/That/Chars/ThatNullableChar.IsALetter.cs +++ b/Source/aweXpect/That/Chars/ThatNullableChar.IsALetter.cs @@ -2,47 +2,15 @@ using aweXpect.Core.Constraints; using aweXpect.Helpers; using aweXpect.Results; +using aweXpect.SourceGenerators; namespace aweXpect; -public static partial class ThatNullableChar -{ - /// - /// Verifies that the subject is a letter. - /// - /// - /// This means, that the specified Unicode character is categorized as a Unicode letter.
- /// - ///
- public static AndOrResult> IsALetter(this IThat source) - => new(source.Get().ExpectationBuilder.AddConstraint((it, grammars) => - new IsALetterConstraint(it, grammars)), - source); - - private sealed class IsALetterConstraint(string it, ExpectationGrammars grammars) - : ConstraintResult.WithNotNullValue(it, grammars), - IValueConstraint - { - public ConstraintResult IsMetBy(char? actual) - { - Actual = actual; - Outcome = actual is not null && char.IsLetter(actual.Value) ? Outcome.Success : Outcome.Failure; - return this; - } - - protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null) - => stringBuilder.Append("is a letter"); - - 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("is not a letter"); - - protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null) - => AppendNormalResult(stringBuilder, indentation); - } -} +[CreateExpectationOnNullable("Is{Not}ALetter", "char.IsLetter({value})", + ExpectationText = "is {not} a letter", + Remarks = """ + This means, that the specified Unicode character is categorized as a Unicode letter.
+ + """ +)] +public static partial class ThatNullableChar; diff --git a/Source/aweXpect/That/Chars/ThatNullableChar.IsANumber.cs b/Source/aweXpect/That/Chars/ThatNullableChar.IsANumber.cs index 8080c97fc..78687201e 100644 --- a/Source/aweXpect/That/Chars/ThatNullableChar.IsANumber.cs +++ b/Source/aweXpect/That/Chars/ThatNullableChar.IsANumber.cs @@ -2,47 +2,15 @@ using aweXpect.Core.Constraints; using aweXpect.Helpers; using aweXpect.Results; +using aweXpect.SourceGenerators; namespace aweXpect; -public static partial class ThatNullableChar -{ - /// - /// Verifies that the subject is a number. - /// - /// - /// This means, that the specified Unicode character is categorized as a number.
- /// - ///
- public static AndOrResult> IsANumber(this IThat source) - => new(source.Get().ExpectationBuilder.AddConstraint((it, grammars) => - new IsANumberConstraint(it, grammars)), - source); - - private sealed class IsANumberConstraint(string it, ExpectationGrammars grammars) - : ConstraintResult.WithNotNullValue(it, grammars), - IValueConstraint - { - public ConstraintResult IsMetBy(char? actual) - { - Actual = actual; - Outcome = actual is not null && char.IsNumber(actual.Value) ? Outcome.Success : Outcome.Failure; - return this; - } - - protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null) - => stringBuilder.Append("is a number"); - - 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("is not a number"); - - protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null) - => AppendNormalResult(stringBuilder, indentation); - } -} +[CreateExpectationOnNullable("Is{Not}ANumber", "char.IsNumber({value})", + ExpectationText = "is {not} a number", + Remarks = """ + This means, that the specified Unicode character is categorized as a number.
+ + """ +)] +public static partial class ThatNullableChar; diff --git a/Source/aweXpect/That/Chars/ThatNullableChar.IsAnAsciiLetter.cs b/Source/aweXpect/That/Chars/ThatNullableChar.IsAnAsciiLetter.cs index c159af8a7..c11ca4364 100644 --- a/Source/aweXpect/That/Chars/ThatNullableChar.IsAnAsciiLetter.cs +++ b/Source/aweXpect/That/Chars/ThatNullableChar.IsAnAsciiLetter.cs @@ -2,56 +2,21 @@ using aweXpect.Core.Constraints; using aweXpect.Helpers; using aweXpect.Results; +using aweXpect.SourceGenerators; namespace aweXpect; -public static partial class ThatNullableChar -{ #if NET8_0_OR_GREATER - /// - /// Verifies that the subject is an ASCII letter. - /// - /// - /// - /// +[CreateExpectationOnNullable("Is{Not}AnAsciiLetter", "char.IsAsciiLetter({value})", + ExpectationText = "is {not} an ASCII letter", + Remarks = """ + This means, that the specified Unicode character is categorized as an ASCII letter.
+ + """ +)] #else - /// - /// Verifies that the subject is an ASCII letter. - /// +[CreateExpectationOnNullable("Is{Not}AnAsciiLetter", "{value} is >= 'a' and <= 'z' or >= 'A' and <= 'Z'", + ExpectationText = "is {not} an ASCII letter" +)] #endif - public static AndOrResult> IsAnAsciiLetter(this IThat source) - => new(source.Get().ExpectationBuilder.AddConstraint((it, grammars) => - new IsAnAsciiLetterConstraint(it, grammars)), - source); - - private sealed class IsAnAsciiLetterConstraint(string it, ExpectationGrammars grammars) - : ConstraintResult.WithNotNullValue(it, grammars), - IValueConstraint - { - public ConstraintResult IsMetBy(char? actual) - { - Actual = actual; -#if NET8_0_OR_GREATER - Outcome = actual is not null && char.IsAsciiLetter(actual.Value) ? Outcome.Success : Outcome.Failure; -#else - Outcome = actual is >= 'a' and <= 'z' or >= 'A' and <= 'Z' ? Outcome.Success : Outcome.Failure; -#endif - return this; - } - - protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null) - => stringBuilder.Append("is an ASCII letter"); - - 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("is not an ASCII letter"); - - protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null) - => AppendNormalResult(stringBuilder, indentation); - } -} +public static partial class ThatNullableChar; diff --git a/Source/aweXpect/That/Chars/ThatNullableChar.IsWhiteSpace.cs b/Source/aweXpect/That/Chars/ThatNullableChar.IsWhiteSpace.cs index 3fdd7792d..f98a91c2f 100644 --- a/Source/aweXpect/That/Chars/ThatNullableChar.IsWhiteSpace.cs +++ b/Source/aweXpect/That/Chars/ThatNullableChar.IsWhiteSpace.cs @@ -2,47 +2,15 @@ using aweXpect.Core.Constraints; using aweXpect.Helpers; using aweXpect.Results; +using aweXpect.SourceGenerators; namespace aweXpect; -public static partial class ThatNullableChar -{ - /// - /// Verifies that the subject is white-space. - /// - /// - /// This means, that the specified Unicode character is categorized as white-space.
- /// - ///
- public static AndOrResult> IsWhiteSpace(this IThat source) - => new(source.Get().ExpectationBuilder.AddConstraint((it, grammars) => - new IsWhiteSpaceConstraint(it, grammars)), - source); - - private sealed class IsWhiteSpaceConstraint(string it, ExpectationGrammars grammars) - : ConstraintResult.WithNotNullValue(it, grammars), - IValueConstraint - { - public ConstraintResult IsMetBy(char? actual) - { - Actual = actual; - Outcome = actual is not null && char.IsWhiteSpace(actual.Value) ? Outcome.Success : Outcome.Failure; - return this; - } - - protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null) - => stringBuilder.Append("is white-space"); - - 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("is not white-space"); - - protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null) - => AppendNormalResult(stringBuilder, indentation); - } -} +[CreateExpectationOnNullable("Is{Not}WhiteSpace", "char.IsWhiteSpace({value})", + ExpectationText = "is {not} white-space", + Remarks = """ + This means, that the specified Unicode character is categorized as white-space.
+ + """ +)] +public static partial class ThatNullableChar; diff --git a/Tests/aweXpect.Api.Tests/Expected/aweXpect_net8.0.txt b/Tests/aweXpect.Api.Tests/Expected/aweXpect_net8.0.txt index 11fc86bc9..d858b14ce 100644 --- a/Tests/aweXpect.Api.Tests/Expected/aweXpect_net8.0.txt +++ b/Tests/aweXpect.Api.Tests/Expected/aweXpect_net8.0.txt @@ -759,10 +759,14 @@ namespace aweXpect public static aweXpect.Results.AndOrResult> IsANumber(this aweXpect.Core.IThat source) { } public static aweXpect.Results.AndOrResult> IsAnAsciiLetter(this aweXpect.Core.IThat source) { } public static aweXpect.Results.AndOrResult> IsEqualTo(this aweXpect.Core.IThat source, char? expected) { } + public static aweXpect.Results.AndOrResult> IsNotALetter(this aweXpect.Core.IThat source) { } + public static aweXpect.Results.AndOrResult> IsNotANumber(this aweXpect.Core.IThat source) { } + public static aweXpect.Results.AndOrResult> IsNotAnAsciiLetter(this aweXpect.Core.IThat source) { } public static aweXpect.Results.AndOrResult> IsNotEqualTo(this aweXpect.Core.IThat source, char? unexpected) { } public static aweXpect.Results.AndOrResult> IsNotOneOf(this aweXpect.Core.IThat source, System.Collections.Generic.IEnumerable unexpected) { } public static aweXpect.Results.AndOrResult> IsNotOneOf(this aweXpect.Core.IThat source, System.Collections.Generic.IEnumerable unexpected) { } public static aweXpect.Results.AndOrResult> IsNotOneOf(this aweXpect.Core.IThat source, params char?[] unexpected) { } + public static aweXpect.Results.AndOrResult> IsNotWhiteSpace(this aweXpect.Core.IThat source) { } public static aweXpect.Results.AndOrResult> IsOneOf(this aweXpect.Core.IThat source, System.Collections.Generic.IEnumerable expected) { } public static aweXpect.Results.AndOrResult> IsOneOf(this aweXpect.Core.IThat source, System.Collections.Generic.IEnumerable expected) { } public static aweXpect.Results.AndOrResult> IsOneOf(this aweXpect.Core.IThat source, params char?[] expected) { } @@ -1458,4 +1462,4 @@ namespace aweXpect.Results public TSelf IgnoringInterspersedItems() { } public TSelf InAnyOrder() { } } -} \ No newline at end of file +} diff --git a/Tests/aweXpect.Api.Tests/Expected/aweXpect_netstandard2.0.txt b/Tests/aweXpect.Api.Tests/Expected/aweXpect_netstandard2.0.txt index 5c4e7c2f1..5fd2f5770 100644 --- a/Tests/aweXpect.Api.Tests/Expected/aweXpect_netstandard2.0.txt +++ b/Tests/aweXpect.Api.Tests/Expected/aweXpect_netstandard2.0.txt @@ -522,10 +522,14 @@ namespace aweXpect public static aweXpect.Results.AndOrResult> IsANumber(this aweXpect.Core.IThat source) { } public static aweXpect.Results.AndOrResult> IsAnAsciiLetter(this aweXpect.Core.IThat source) { } public static aweXpect.Results.AndOrResult> IsEqualTo(this aweXpect.Core.IThat source, char? expected) { } + public static aweXpect.Results.AndOrResult> IsNotALetter(this aweXpect.Core.IThat source) { } + public static aweXpect.Results.AndOrResult> IsNotANumber(this aweXpect.Core.IThat source) { } + public static aweXpect.Results.AndOrResult> IsNotAnAsciiLetter(this aweXpect.Core.IThat source) { } public static aweXpect.Results.AndOrResult> IsNotEqualTo(this aweXpect.Core.IThat source, char? unexpected) { } public static aweXpect.Results.AndOrResult> IsNotOneOf(this aweXpect.Core.IThat source, System.Collections.Generic.IEnumerable unexpected) { } public static aweXpect.Results.AndOrResult> IsNotOneOf(this aweXpect.Core.IThat source, System.Collections.Generic.IEnumerable unexpected) { } public static aweXpect.Results.AndOrResult> IsNotOneOf(this aweXpect.Core.IThat source, params char?[] unexpected) { } + public static aweXpect.Results.AndOrResult> IsNotWhiteSpace(this aweXpect.Core.IThat source) { } public static aweXpect.Results.AndOrResult> IsOneOf(this aweXpect.Core.IThat source, System.Collections.Generic.IEnumerable expected) { } public static aweXpect.Results.AndOrResult> IsOneOf(this aweXpect.Core.IThat source, System.Collections.Generic.IEnumerable expected) { } public static aweXpect.Results.AndOrResult> IsOneOf(this aweXpect.Core.IThat source, params char?[] expected) { } @@ -1386,4 +1390,4 @@ namespace aweXpect.Results public TSelf IgnoringInterspersedItems() { } public TSelf InAnyOrder() { } } -} \ No newline at end of file +} diff --git a/Tests/aweXpect.Tests/Chars/ThatChar.Nullable.IsNotALetter.Tests.cs b/Tests/aweXpect.Tests/Chars/ThatChar.Nullable.IsNotALetter.Tests.cs new file mode 100644 index 000000000..23f894ef3 --- /dev/null +++ b/Tests/aweXpect.Tests/Chars/ThatChar.Nullable.IsNotALetter.Tests.cs @@ -0,0 +1,122 @@ +namespace aweXpect.Tests; + +public sealed partial class ThatChar +{ + public sealed partial class Nullable + { + public sealed class IsNotALetter + { + public sealed class Tests + { + [Theory] + [InlineData('\t')] + [InlineData('5')] + [InlineData('@')] + [InlineData('[')] + [InlineData(']')] + [InlineData('{')] + [InlineData('}')] + public async Task WhenSubjectIsNoLetter_ShouldSucceed(char? subject) + { + async Task Act() + => await That(subject).IsNotALetter(); + + await That(Act).DoesNotThrow(); + } + + [Theory] + [InlineData('a')] + [InlineData('d')] + [InlineData('z')] + [InlineData('A')] + [InlineData('M')] + [InlineData('Z')] + [InlineData('\u4E50')] + public async Task WhenSubjectIsNotALetter_ShouldFail(char? subject) + { + async Task Act() + => await That(subject).IsNotALetter(); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + is not a letter, + but it was {Formatter.Format(subject)} + """); + } + + [Fact] + public async Task WhenSubjectIsNull_ShouldFail() + { + char? subject = null; + + async Task Act() + => await That(subject).IsNotALetter(); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + is not a letter, + but it was + """); + } + } + + public sealed class NegatedTests + { + [Theory] + [InlineData('\t')] + [InlineData('5')] + [InlineData('@')] + [InlineData('[')] + [InlineData(']')] + [InlineData('{')] + [InlineData('}')] + public async Task WhenSubjectIsNoLetter_ShouldFail(char? subject) + { + async Task Act() + => await That(subject).DoesNotComplyWith(it => it.IsNotALetter()); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + is a letter, + but it was {Formatter.Format(subject)} + """); + } + + [Theory] + [InlineData('a')] + [InlineData('d')] + [InlineData('z')] + [InlineData('A')] + [InlineData('M')] + [InlineData('Z')] + [InlineData('\u4E50')] + public async Task WhenSubjectIsNotALetter_ShouldSucceed(char? subject) + { + async Task Act() + => await That(subject).DoesNotComplyWith(it => it.IsNotALetter()); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenSubjectIsNull_ShouldFail() + { + char? subject = null; + + async Task Act() + => await That(subject).DoesNotComplyWith(it => it.IsNotALetter()); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + is a letter, + but it was + """); + } + } + } + } +} diff --git a/Tests/aweXpect.Tests/Chars/ThatChar.Nullable.IsNotANumber.Tests.cs b/Tests/aweXpect.Tests/Chars/ThatChar.Nullable.IsNotANumber.Tests.cs new file mode 100644 index 000000000..634288bb1 --- /dev/null +++ b/Tests/aweXpect.Tests/Chars/ThatChar.Nullable.IsNotANumber.Tests.cs @@ -0,0 +1,128 @@ +namespace aweXpect.Tests; + +public sealed partial class ThatChar +{ + public sealed partial class Nullable + { + public sealed class IsNotANumber + { + public sealed class Tests + { + [Theory] + [InlineData('a')] + [InlineData('d')] + [InlineData('z')] + [InlineData('A')] + [InlineData('M')] + [InlineData('Z')] + [InlineData('\u4E50')] + [InlineData('\t')] + [InlineData('@')] + [InlineData('[')] + [InlineData(']')] + [InlineData('{')] + [InlineData('}')] + public async Task WhenSubjectIsNoLetter_ShouldSucceed(char? subject) + { + async Task Act() + => await That(subject).IsNotANumber(); + + await That(Act).DoesNotThrow(); + } + + [Theory] + [InlineData('0')] + [InlineData('1')] + [InlineData('4')] + [InlineData('9')] + public async Task WhenSubjectIsNotANumber_ShouldFail(char? subject) + { + async Task Act() + => await That(subject).IsNotANumber(); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + is not a number, + but it was {Formatter.Format(subject)} + """); + } + + [Fact] + public async Task WhenSubjectIsNull_ShouldFail() + { + char? subject = null; + + async Task Act() + => await That(subject).IsNotANumber(); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + is not a number, + but it was + """); + } + } + + public sealed class NegatedTests + { + [Theory] + [InlineData('a')] + [InlineData('d')] + [InlineData('z')] + [InlineData('A')] + [InlineData('M')] + [InlineData('Z')] + [InlineData('\u4E50')] + [InlineData('\t')] + [InlineData('@')] + [InlineData('[')] + [InlineData(']')] + [InlineData('{')] + [InlineData('}')] + public async Task WhenSubjectIsNoLetter_ShouldFail(char? subject) + { + async Task Act() + => await That(subject).DoesNotComplyWith(it => it.IsNotANumber()); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + is a number, + but it was {Formatter.Format(subject)} + """); + } + + [Theory] + [InlineData('0')] + [InlineData('1')] + [InlineData('4')] + [InlineData('9')] + public async Task WhenSubjectIsNotANumber_ShouldSucceed(char? subject) + { + async Task Act() + => await That(subject).DoesNotComplyWith(it => it.IsNotANumber()); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenSubjectIsNull_ShouldFail() + { + char? subject = null; + + async Task Act() + => await That(subject).DoesNotComplyWith(it => it.IsNotANumber()); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + is a number, + but it was + """); + } + } + } + } +} diff --git a/Tests/aweXpect.Tests/Chars/ThatChar.Nullable.IsNotAnAsciiLetter.Tests.cs b/Tests/aweXpect.Tests/Chars/ThatChar.Nullable.IsNotAnAsciiLetter.Tests.cs new file mode 100644 index 000000000..e0344c458 --- /dev/null +++ b/Tests/aweXpect.Tests/Chars/ThatChar.Nullable.IsNotAnAsciiLetter.Tests.cs @@ -0,0 +1,122 @@ +namespace aweXpect.Tests; + +public sealed partial class ThatChar +{ + public sealed partial class Nullable + { + public sealed class IsNotAnAsciiLetter + { + public sealed class Tests + { + [Theory] + [InlineData('\t')] + [InlineData('5')] + [InlineData('@')] + [InlineData('[')] + [InlineData(']')] + [InlineData('{')] + [InlineData('}')] + [InlineData('\u4E50')] + public async Task WhenSubjectIsNoAsciiLetter_ShouldSucceed(char? subject) + { + async Task Act() + => await That(subject).IsNotAnAsciiLetter(); + + await That(Act).DoesNotThrow(); + } + + [Theory] + [InlineData('a')] + [InlineData('d')] + [InlineData('z')] + [InlineData('A')] + [InlineData('M')] + [InlineData('Z')] + public async Task WhenSubjectIsNotAnAsciiLetter_ShouldFail(char? subject) + { + async Task Act() + => await That(subject).IsNotAnAsciiLetter(); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + is not an ASCII letter, + but it was {Formatter.Format(subject)} + """); + } + + [Fact] + public async Task WhenSubjectIsNull_ShouldFail() + { + char? subject = null; + + async Task Act() + => await That(subject).IsNotAnAsciiLetter(); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + is not an ASCII letter, + but it was + """); + } + } + + public sealed class NegatedTests + { + [Theory] + [InlineData('\t')] + [InlineData('5')] + [InlineData('@')] + [InlineData('[')] + [InlineData(']')] + [InlineData('{')] + [InlineData('}')] + [InlineData('\u4E50')] + public async Task WhenSubjectIsNoAsciiLetter_ShouldFail(char? subject) + { + async Task Act() + => await That(subject).DoesNotComplyWith(it => it.IsNotAnAsciiLetter()); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + is an ASCII letter, + but it was {Formatter.Format(subject)} + """); + } + + [Theory] + [InlineData('a')] + [InlineData('d')] + [InlineData('z')] + [InlineData('A')] + [InlineData('M')] + [InlineData('Z')] + public async Task WhenSubjectIsNotAnAsciiLetter_ShouldSucceed(char? subject) + { + async Task Act() + => await That(subject).DoesNotComplyWith(it => it.IsNotAnAsciiLetter()); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenSubjectIsNull_ShouldFail() + { + char? subject = null; + + async Task Act() + => await That(subject).DoesNotComplyWith(it => it.IsNotAnAsciiLetter()); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + is an ASCII letter, + but it was + """); + } + } + } + } +} diff --git a/Tests/aweXpect.Tests/Chars/ThatChar.Nullable.IsNotWhiteSpace.Tests.cs b/Tests/aweXpect.Tests/Chars/ThatChar.Nullable.IsNotWhiteSpace.Tests.cs new file mode 100644 index 000000000..56ad37081 --- /dev/null +++ b/Tests/aweXpect.Tests/Chars/ThatChar.Nullable.IsNotWhiteSpace.Tests.cs @@ -0,0 +1,134 @@ +namespace aweXpect.Tests; + +public sealed partial class ThatChar +{ + public sealed partial class Nullable + { + public sealed class IsNotWhiteSpace + { + public sealed class Tests + { + [Theory] + [InlineData(' ')] + [InlineData('\t')] + [InlineData('\r')] + [InlineData('\n')] + public async Task WhenSubjectIsNotWhiteSpace_ShouldFail(char? subject) + { + async Task Act() + => await That(subject).IsNotWhiteSpace(); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + is not white-space, + but it was {Formatter.Format(subject)} + """); + } + + [Theory] + [InlineData('0')] + [InlineData('1')] + [InlineData('4')] + [InlineData('9')] + [InlineData('a')] + [InlineData('d')] + [InlineData('z')] + [InlineData('A')] + [InlineData('M')] + [InlineData('Z')] + [InlineData('\u4E50')] + [InlineData('@')] + [InlineData('[')] + [InlineData(']')] + [InlineData('{')] + [InlineData('}')] + public async Task WhenSubjectIsNotWhiteSpace_ShouldSucceed(char? subject) + { + async Task Act() + => await That(subject).IsNotWhiteSpace(); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenSubjectIsNull_ShouldFail() + { + char? subject = null; + + async Task Act() + => await That(subject).IsNotWhiteSpace(); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + is not white-space, + but it was + """); + } + } + + public sealed class NegatedTests + { + [Theory] + [InlineData('0')] + [InlineData('1')] + [InlineData('4')] + [InlineData('9')] + [InlineData('a')] + [InlineData('d')] + [InlineData('z')] + [InlineData('A')] + [InlineData('M')] + [InlineData('Z')] + [InlineData('\u4E50')] + [InlineData('@')] + [InlineData('[')] + [InlineData(']')] + [InlineData('{')] + [InlineData('}')] + public async Task WhenSubjectIsNotWhiteSpace_ShouldFail(char? subject) + { + async Task Act() + => await That(subject).DoesNotComplyWith(it => it.IsNotWhiteSpace()); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + is white-space, + but it was {Formatter.Format(subject)} + """); + } + + [Theory] + [InlineData(' ')] + [InlineData('\t')] + [InlineData('\r')] + [InlineData('\n')] + public async Task WhenSubjectIsNotWhiteSpace_ShouldSucceed(char? subject) + { + async Task Act() + => await That(subject).DoesNotComplyWith(it => it.IsNotWhiteSpace()); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenSubjectIsNull_ShouldFail() + { + char? subject = null; + + async Task Act() + => await That(subject).DoesNotComplyWith(it => it.IsNotWhiteSpace()); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + is white-space, + but it was + """); + } + } + } + } +} From d6151f11f70978822ab0577bf27e806a8852b6ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sun, 14 Sep 2025 06:38:19 +0200 Subject: [PATCH 2/3] Cleanup --- .../ExpectationGenerator.cs | 15 ++++++++++----- .../{ => Helpers}/ExpectationToGenerate.cs | 2 +- .../{ => Helpers}/SourceGenerationHelper.cs | 2 +- .../Expected/aweXpect_net8.0.txt | 2 +- .../Expected/aweXpect_netstandard2.0.txt | 2 +- 5 files changed, 14 insertions(+), 9 deletions(-) rename Source/aweXpect.SourceGenerators/{ => Helpers}/ExpectationToGenerate.cs (97%) rename Source/aweXpect.SourceGenerators/{ => Helpers}/SourceGenerationHelper.cs (99%) diff --git a/Source/aweXpect.SourceGenerators/ExpectationGenerator.cs b/Source/aweXpect.SourceGenerators/ExpectationGenerator.cs index f57d2d109..54c224873 100644 --- a/Source/aweXpect.SourceGenerators/ExpectationGenerator.cs +++ b/Source/aweXpect.SourceGenerators/ExpectationGenerator.cs @@ -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; @@ -12,11 +13,12 @@ namespace aweXpect.SourceGenerators; [Generator] public class ExpectationGenerator : IIncrementalGenerator { - private static string[] _supportedAttributes = + private static readonly string[] _supportedAttributes = [ nameof(SourceGenerationHelper.CreateExpectationOnAttribute), nameof(SourceGenerationHelper.CreateExpectationOnNullableAttribute), ]; + void IIncrementalGenerator.Initialize(IncrementalGeneratorInitializationContext context) { // Add the marker attributes to the compilation @@ -70,8 +72,7 @@ private static IEnumerable GetSemanticTargetForGeneration continue; } - ExpectationToGenerate? expectationToGenerate = GetExpectationToGenerate(classSymbol, - attributeData); + ExpectationToGenerate? expectationToGenerate = GetExpectationToGenerate(classSymbol, attributeData); if (expectationToGenerate != null && files.Add(expectationToGenerate.Value.FileName)) { @@ -120,8 +121,12 @@ private static void Execute(ExpectationToGenerate expectationToGenerate, SourceP return null; } - - return new ExpectationToGenerate(containingNamespace, classSymbol.Name, targetType, name, outcomeMethod, + return new ExpectationToGenerate( + containingNamespace, + classSymbol.Name, + targetType, + name, + outcomeMethod, attributeData); } } diff --git a/Source/aweXpect.SourceGenerators/ExpectationToGenerate.cs b/Source/aweXpect.SourceGenerators/Helpers/ExpectationToGenerate.cs similarity index 97% rename from Source/aweXpect.SourceGenerators/ExpectationToGenerate.cs rename to Source/aweXpect.SourceGenerators/Helpers/ExpectationToGenerate.cs index 624b38a25..b591342fc 100644 --- a/Source/aweXpect.SourceGenerators/ExpectationToGenerate.cs +++ b/Source/aweXpect.SourceGenerators/Helpers/ExpectationToGenerate.cs @@ -1,6 +1,6 @@ using Microsoft.CodeAnalysis; -namespace aweXpect.SourceGenerators; +namespace aweXpect.SourceGenerators.Helpers; internal readonly record struct ExpectationToGenerate { diff --git a/Source/aweXpect.SourceGenerators/SourceGenerationHelper.cs b/Source/aweXpect.SourceGenerators/Helpers/SourceGenerationHelper.cs similarity index 99% rename from Source/aweXpect.SourceGenerators/SourceGenerationHelper.cs rename to Source/aweXpect.SourceGenerators/Helpers/SourceGenerationHelper.cs index 330aa0031..c27eb08fa 100644 --- a/Source/aweXpect.SourceGenerators/SourceGenerationHelper.cs +++ b/Source/aweXpect.SourceGenerators/Helpers/SourceGenerationHelper.cs @@ -1,4 +1,4 @@ -namespace aweXpect.SourceGenerators; +namespace aweXpect.SourceGenerators.Helpers; internal static class SourceGenerationHelper { diff --git a/Tests/aweXpect.Api.Tests/Expected/aweXpect_net8.0.txt b/Tests/aweXpect.Api.Tests/Expected/aweXpect_net8.0.txt index d858b14ce..23f05b64d 100644 --- a/Tests/aweXpect.Api.Tests/Expected/aweXpect_net8.0.txt +++ b/Tests/aweXpect.Api.Tests/Expected/aweXpect_net8.0.txt @@ -1462,4 +1462,4 @@ namespace aweXpect.Results public TSelf IgnoringInterspersedItems() { } public TSelf InAnyOrder() { } } -} +} \ No newline at end of file diff --git a/Tests/aweXpect.Api.Tests/Expected/aweXpect_netstandard2.0.txt b/Tests/aweXpect.Api.Tests/Expected/aweXpect_netstandard2.0.txt index 5fd2f5770..341c17493 100644 --- a/Tests/aweXpect.Api.Tests/Expected/aweXpect_netstandard2.0.txt +++ b/Tests/aweXpect.Api.Tests/Expected/aweXpect_netstandard2.0.txt @@ -1390,4 +1390,4 @@ namespace aweXpect.Results public TSelf IgnoringInterspersedItems() { } public TSelf InAnyOrder() { } } -} +} \ No newline at end of file From e4ad30f9e147bb9fc0c2f6598f6aec101dd85fd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sun, 14 Sep 2025 06:41:53 +0200 Subject: [PATCH 3/3] Also use attributes for nullable `Guid.IsEmpty` --- .../That/Guids/ThatNullableGuid.IsEmpty.cs | 55 ++----------------- 1 file changed, 6 insertions(+), 49 deletions(-) diff --git a/Source/aweXpect/That/Guids/ThatNullableGuid.IsEmpty.cs b/Source/aweXpect/That/Guids/ThatNullableGuid.IsEmpty.cs index 361a89d58..83104fb84 100644 --- a/Source/aweXpect/That/Guids/ThatNullableGuid.IsEmpty.cs +++ b/Source/aweXpect/That/Guids/ThatNullableGuid.IsEmpty.cs @@ -1,53 +1,10 @@ using System; -using aweXpect.Core; -using aweXpect.Core.Constraints; -using aweXpect.Helpers; -using aweXpect.Results; +using aweXpect.SourceGenerators; namespace aweXpect; -public static partial class ThatNullableGuid -{ - /// - /// Verifies that the subject is empty. - /// - public static AndOrResult> IsEmpty(this IThat source) - => new(source.Get().ExpectationBuilder.AddConstraint((it, grammars) => - new IsEmptyConstraint(it, grammars)), - source); - - /// - /// Verifies that the subject is not empty. - /// - public static AndOrResult> IsNotEmpty(this IThat source) - => new(source.Get().ExpectationBuilder.AddConstraint((it, grammars) => - new IsEmptyConstraint(it, grammars).Invert()), - source); - - private sealed class IsEmptyConstraint(string it, ExpectationGrammars grammars) - : ConstraintResult.WithNotNullValue(it, grammars), - IValueConstraint - { - public ConstraintResult IsMetBy(Guid? actual) - { - Actual = actual; - Outcome = actual == Guid.Empty ? Outcome.Success : Outcome.Failure; - return this; - } - - protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null) - => stringBuilder.Append("is empty"); - - 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("is not empty"); - - protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null) - => AppendNormalResult(stringBuilder, indentation); - } -} +[CreateExpectationOnNullable("Is{Not}Empty", "{value} == Guid.Empty", + ExpectationText = "is {not} empty", + Using = ["System",] +)] +public static partial class ThatNullableGuid;