diff --git a/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/AlternativeMonad/MatchToOrElseCodeFix.cs b/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/AlternativeMonad/MatchToOrElseCodeFix.cs index 96cf5761..dc31abaa 100644 --- a/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/AlternativeMonad/MatchToOrElseCodeFix.cs +++ b/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/AlternativeMonad/MatchToOrElseCodeFix.cs @@ -26,8 +26,7 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) foreach (var diagnostic in context.Diagnostics) { if (syntaxRoot?.FindInvocationExpression(context.Span) is { Expression: MemberAccessExpressionSyntax memberAccessExpression } invocation - && diagnostic.Properties.TryGetValue(PreservedArgumentIndexProperty, out var errorStateArgumentIndexString) - && int.TryParse(errorStateArgumentIndexString, out var noneArgumentIndex)) + && diagnostic.TryGetIntProperty(PreservedArgumentIndexProperty, out var noneArgumentIndex)) { context.RegisterCodeFix(new GetOrElseCodeFixAction(context.Document, invocation, memberAccessExpression, noneArgumentIndex, DiagnosticIdToMethodName(diagnostic.Id)), diagnostic); } diff --git a/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/DiagnosticExtensions.cs b/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/DiagnosticExtensions.cs new file mode 100644 index 00000000..a8f019c6 --- /dev/null +++ b/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/DiagnosticExtensions.cs @@ -0,0 +1,19 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; + +namespace Funcky.Analyzers; + +internal static class DiagnosticExtensions +{ + // No static interfaces or IParsable in .NET Standard 2.0... + private delegate bool TryParseDelegate(string? s, [NotNullWhen(true)] out T? value); + + public static bool TryGetIntProperty(this Diagnostic diagnostic, string propertyName, out int value) + => TryGetProperty(diagnostic, propertyName, int.TryParse, out value); + + private static bool TryGetProperty(this Diagnostic diagnostic, string propertyName, TryParseDelegate parser, [NotNullWhen(true)] out T? value) + { + value = default; + return diagnostic.Properties.TryGetValue(propertyName, out var stringValue) && parser(stringValue, out value); + } +} diff --git a/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/EnumerableRepeatNeverCodeFix.cs b/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/EnumerableRepeatNeverCodeFix.cs index 3e1cd7c5..70dd5ba8 100644 --- a/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/EnumerableRepeatNeverCodeFix.cs +++ b/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/EnumerableRepeatNeverCodeFix.cs @@ -30,8 +30,7 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) var diagnosticSpan = diagnostic.Location.SourceSpan; if (root?.FindToken(diagnosticSpan.Start).Parent?.AncestorsAndSelf().OfType().First() is { } declaration - && diagnostic.Properties.TryGetValue(ValueParameterIndexProperty, out var valueParameterIndexProperty) - && int.TryParse(valueParameterIndexProperty, out var valueParameterIndex)) + && diagnostic.TryGetIntProperty(ValueParameterIndexProperty, out var valueParameterIndex)) { context.RegisterCodeFix(new ToEnumerableEmptyCodeAction(context.Document, declaration, valueParameterIndex), diagnostic); } diff --git a/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/EnumerableRepeatOnceCodeFix.cs b/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/EnumerableRepeatOnceCodeFix.cs index 00669f51..511c390e 100644 --- a/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/EnumerableRepeatOnceCodeFix.cs +++ b/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/EnumerableRepeatOnceCodeFix.cs @@ -31,8 +31,7 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) var diagnosticSpan = diagnostic.Location.SourceSpan; if (root?.FindToken(diagnosticSpan.Start).Parent?.AncestorsAndSelf().OfType().First() is { } declaration - && diagnostic.Properties.TryGetValue(ValueParameterIndexProperty, out var valueParameterIndexProperty) - && int.TryParse(valueParameterIndexProperty, out var valueParameterIndex)) + && diagnostic.TryGetIntProperty(ValueParameterIndexProperty, out var valueParameterIndex)) { context.RegisterCodeFix(new ToSequenceReturnCodeAction(context.Document, declaration, valueParameterIndex), diagnostic); } diff --git a/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/FunctionalAssertFix.cs b/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/FunctionalAssertFix.cs new file mode 100644 index 00000000..4e4c148c --- /dev/null +++ b/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/FunctionalAssertFix.cs @@ -0,0 +1,76 @@ +using System.Collections.Immutable; +using System.Composition; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Funcky.Analyzers.FunctionalAssert.FunctionalAssertAnalyzer; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace Funcky.Analyzers; + +[Shared] +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AddArgumentNameCodeFix))] +public sealed class FunctionalAssertFix : CodeFixProvider +{ + public override ImmutableArray FixableDiagnosticIds + => ImmutableArray.Create(PreferFunctionalAssert.Id); + + public override FixAllProvider GetFixAllProvider() + => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + if (await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false) is { } root + && await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false) is { } semanticModel) + { + foreach (var diagnostic in context.Diagnostics) + { + if (diagnostic.TryGetIntProperty(ExpectedArgumentIndex, out var expectedArgumentIndex) + && diagnostic.TryGetIntProperty(ActualArgumentIndex, out var actualArgumentIndex) + && root.FindNode(diagnostic.Location.SourceSpan).FirstAncestorOrSelf() is { } invocationSyntax + && invocationSyntax.ArgumentList.Arguments[expectedArgumentIndex] is var expectedArgumentSyntax + && invocationSyntax.ArgumentList.Arguments[actualArgumentIndex] is var actualArgumentSyntax + && actualArgumentSyntax.Expression is InvocationExpressionSyntax innerInvocationSyntax) + { + context.RegisterCodeFix(CreateFix(context, new FixInputs(invocationSyntax, expectedArgumentSyntax, actualArgumentSyntax, innerInvocationSyntax)), diagnostic); + } + } + } + } + + private static CodeAction CreateFix(CodeFixContext context, FixInputs inputs) + => CodeAction.Create( + title: "Simplify assertion", + SimplifyAssertionAsync(context.Document, inputs), + nameof(AddArgumentNameCodeFix)); + + private static Func> SimplifyAssertionAsync(Document document, FixInputs inputs) + => async cancellationToken + => await document.GetSyntaxRootAsync(cancellationToken) is { } syntaxRoot + ? document.WithSyntaxRoot(syntaxRoot.ReplaceNode(inputs.InvocationSyntax, SimplifyAssertion(inputs))) + : document; + + private static InvocationExpressionSyntax SimplifyAssertion(FixInputs inputs) + => InvocationExpression(inputs.InnerInvocationSyntax.Expression) + .WithArgumentList(FunctionalAssertArgumentList(inputs)) + .WithTriviaFrom(inputs.InvocationSyntax); + + private static ArgumentListSyntax FunctionalAssertArgumentList(FixInputs inputs) + { + var assertArgumentList = inputs.InvocationSyntax.ArgumentList; + return assertArgumentList.WithArguments(SeparatedList(FunctionalAssertArguments(inputs), assertArgumentList.Arguments.GetSeparators())); + } + + private static IEnumerable FunctionalAssertArguments(FixInputs inputs) + => [ + Argument(inputs.ExpectedArgumentSyntax.Expression).WithTriviaFrom(inputs.ExpectedArgumentSyntax), + Argument(inputs.InnerInvocationSyntax.ArgumentList.Arguments[0].Expression).WithTriviaFrom(inputs.ActualArgumentSyntax), + ]; + + private sealed record FixInputs( + InvocationExpressionSyntax InvocationSyntax, + ArgumentSyntax ExpectedArgumentSyntax, + ArgumentSyntax ActualArgumentSyntax, + InvocationExpressionSyntax InnerInvocationSyntax); +} diff --git a/Funcky.Analyzers/Funcky.Analyzers.Test/FunctionalAssertAnalyzerTest.Setup.cs b/Funcky.Analyzers/Funcky.Analyzers.Test/FunctionalAssertAnalyzerTest.Setup.cs new file mode 100644 index 00000000..a29bd6a8 --- /dev/null +++ b/Funcky.Analyzers/Funcky.Analyzers.Test/FunctionalAssertAnalyzerTest.Setup.cs @@ -0,0 +1,45 @@ +namespace Funcky.Analyzers.Test; + +public sealed partial class FunctionalAssertAnalyzerTest +{ + // language=csharp + private const string AttributeSource = + """ + namespace Funcky.CodeAnalysis + { + internal sealed class AssertMethodHasOverloadWithExpectedValueAttribute : System.Attribute { } + } + """; + + // language=csharp + private const string Stubs = + """ + namespace Xunit + { + public static class Assert + { + public static void Equal(T expected, T actual) { } + + public static void Equal(T expected, T actual, System.Func compare) { } + + public static void Equal(System.DateTime expected, System.DateTime actual) { } + } + } + + namespace Funcky.Monads + { + public readonly struct Option { } + } + + namespace Funcky + { + public static class FunctionalAssert + { + [Funcky.CodeAnalysis.AssertMethodHasOverloadWithExpectedValueAttribute] + public static T Some(Option option) => throw null!; + + public static T Some(T expected, Option option) => throw null!; + } + } + """; +} diff --git a/Funcky.Analyzers/Funcky.Analyzers.Test/FunctionalAssertAnalyzerTest.cs b/Funcky.Analyzers/Funcky.Analyzers.Test/FunctionalAssertAnalyzerTest.cs new file mode 100644 index 00000000..e197a9ae --- /dev/null +++ b/Funcky.Analyzers/Funcky.Analyzers.Test/FunctionalAssertAnalyzerTest.cs @@ -0,0 +1,90 @@ +using Microsoft.CodeAnalysis.Testing; +using Xunit; +using VerifyCS = Funcky.Analyzers.Test.CSharpCodeFixVerifier; + +namespace Funcky.Analyzers.Test; + +public sealed partial class FunctionalAssertAnalyzerTest +{ + [Fact] + public async Task WarnsWhenCombiningAssertEqualWithOurMethod() + { + // language=csharp + const string inputCode = + """ + using Funcky; + using Funcky.Monads; + using Xunit; + + class C + { + private void M() + { + Assert.Equal(42, FunctionalAssert.Some(default(Option))); + Assert.Equal( + 42, + FunctionalAssert.Some(default(Option))); + Assert.Equal(42, FunctionalAssert.Some( + default(Option) + )); + Assert.Equal(actual: FunctionalAssert.Some(default(Option)), expected: 42); + } + } + """; + + // language=csharp + const string fixedCode = + """ + using Funcky; + using Funcky.Monads; + using Xunit; + + class C + { + private void M() + { + FunctionalAssert.Some(42, default(Option)); + FunctionalAssert.Some( + 42, + default(Option)); + FunctionalAssert.Some(42, default(Option)); + FunctionalAssert.Some(42, default(Option)); + } + } + """; + DiagnosticResult[] expectedDiagnostics = [ + VerifyCS.Diagnostic().WithSpan(9, 9, 9, 70).WithArguments("FunctionalAssert", "Some"), + VerifyCS.Diagnostic().WithSpan(10, 9, 12, 57).WithArguments("FunctionalAssert", "Some"), + VerifyCS.Diagnostic().WithSpan(13, 9, 15, 11).WithArguments("FunctionalAssert", "Some"), + VerifyCS.Diagnostic().WithSpan(16, 9, 16, 88).WithArguments("FunctionalAssert", "Some"), + ]; + + await VerifyCS.VerifyCodeFixAsync( + inputCode + Environment.NewLine + AttributeSource + Stubs, + expectedDiagnostics, + fixedCode + Environment.NewLine + AttributeSource + Stubs); + } + + [Fact] + public async Task DoesNotWarnForSpecializedOverloadsOfAssertEqual() + { + // language=csharp + const string inputCode = + """ + using System; + using Funcky; + using Funcky.Monads; + using Xunit; + + class C + { + private void M() + { + Assert.Equal(DateTime.UnixEpoch, FunctionalAssert.Some(default(Option))); + Assert.Equal(42, FunctionalAssert.Some(default(Option)), (a, b) => throw null!); + } + } + """; + await VerifyCS.VerifyAnalyzerAsync(inputCode + AttributeSource + Stubs); + } +} diff --git a/Funcky.Analyzers/Funcky.Analyzers/AnalyzerReleases.Unshipped.md b/Funcky.Analyzers/Funcky.Analyzers/AnalyzerReleases.Unshipped.md index eefd4b5d..417828f3 100644 --- a/Funcky.Analyzers/Funcky.Analyzers/AnalyzerReleases.Unshipped.md +++ b/Funcky.Analyzers/Funcky.Analyzers/AnalyzerReleases.Unshipped.md @@ -4,3 +4,4 @@ ### New Rules Rule ID | Category | Severity | Notes --------|----------|----------|------- +λ1101 | Funcky | Warning | FunctionalAssertAnalyzer diff --git a/Funcky.Analyzers/Funcky.Analyzers/DiagnosticName.cs b/Funcky.Analyzers/Funcky.Analyzers/DiagnosticName.cs index 706d8f7d..6e88347c 100644 --- a/Funcky.Analyzers/Funcky.Analyzers/DiagnosticName.cs +++ b/Funcky.Analyzers/Funcky.Analyzers/DiagnosticName.cs @@ -5,4 +5,6 @@ internal static class DiagnosticName public const string Prefix = "λ"; public const string Usage = "10"; + + public const string FunctionalAssertUsage = "11"; } diff --git a/Funcky.Analyzers/Funcky.Analyzers/FunckyWellKnownMemberNames.cs b/Funcky.Analyzers/Funcky.Analyzers/FunckyWellKnownMemberNames.cs index 11244a0c..99a8861a 100644 --- a/Funcky.Analyzers/Funcky.Analyzers/FunckyWellKnownMemberNames.cs +++ b/Funcky.Analyzers/Funcky.Analyzers/FunckyWellKnownMemberNames.cs @@ -38,4 +38,9 @@ public static class FunckyWellKnownMemberNames /// The ToNullable method on alternative monad types. public const string ToNullableMethodName = "ToNullable"; + + public static class XunitAssert + { + public const string EqualMethodName = "Equal"; + } } diff --git a/Funcky.Analyzers/Funcky.Analyzers/FunckyWellKnownTypeNames.cs b/Funcky.Analyzers/Funcky.Analyzers/FunckyWellKnownTypeNames.cs index 351354e3..c48da658 100644 --- a/Funcky.Analyzers/Funcky.Analyzers/FunckyWellKnownTypeNames.cs +++ b/Funcky.Analyzers/Funcky.Analyzers/FunckyWellKnownTypeNames.cs @@ -27,4 +27,6 @@ public static class FunckyWellKnownTypeNames public static INamedTypeSymbol? GetFunctionalType(this Compilation compilation) => compilation.GetTypeByMetadataName("Funcky.Functional"); public static INamedTypeSymbol? GetExpressionOfTType(this Compilation compilation) => compilation.GetTypeByMetadataName("System.Linq.Expressions.Expression`1"); + + public static INamedTypeSymbol? GetXunitAssertType(this Compilation compilation) => compilation.GetTypeByMetadataName("Xunit.Assert"); } diff --git a/Funcky.Analyzers/Funcky.Analyzers/FunctionalAssert/FunctionalAssertAnalyzer.cs b/Funcky.Analyzers/Funcky.Analyzers/FunctionalAssert/FunctionalAssertAnalyzer.cs new file mode 100644 index 00000000..a47bc046 --- /dev/null +++ b/Funcky.Analyzers/Funcky.Analyzers/FunctionalAssert/FunctionalAssertAnalyzer.cs @@ -0,0 +1,61 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; +using static Funcky.Analyzers.FunctionalAssert.FunctionalAssertMatching; +using static Funcky.Analyzers.FunctionalAssert.XunitAssertMatching; + +namespace Funcky.Analyzers.FunctionalAssert; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class FunctionalAssertAnalyzer : DiagnosticAnalyzer +{ + public const string ExpectedArgumentIndex = nameof(ExpectedArgumentIndex); + public const string ActualArgumentIndex = nameof(ActualArgumentIndex); + + public static readonly DiagnosticDescriptor PreferFunctionalAssert = new( + id: $"{DiagnosticName.Prefix}{DiagnosticName.FunctionalAssertUsage}01", + title: "Assert can be simplified", + messageFormat: "Assert can be simplified to a single call to {0}.{1}", + category: nameof(Funcky), + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + private const string AttributeFullName = "Funcky.CodeAnalysis.AssertMethodHasOverloadWithExpectedValueAttribute"; + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(PreferFunctionalAssert); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterCompilationStartAction(OnCompilationStart); + } + + private static void OnCompilationStart(CompilationStartAnalysisContext context) + { + if (context.Compilation.GetTypeByMetadataName(AttributeFullName) is { } attributeType) + { + context.RegisterOperationAction(AnalyzeInvocation(new AssertMethodHasOverloadWithExpectedValueAttributeType(attributeType)), OperationKind.Invocation); + } + } + + private static Action AnalyzeInvocation(AssertMethodHasOverloadWithExpectedValueAttributeType attributeType) + => context => + { + var invocation = (IInvocationOperation)context.Operation; + if (MatchGenericAssertEqualInvocation(invocation, out var expectedArgument, out var actualArgument) + && actualArgument.Value is IInvocationOperation innerInvocation + && IsAssertMethodWithAccompanyingEqualOverload(innerInvocation, attributeType)) + { + var properties = ImmutableDictionary.Empty + .Add(ActualArgumentIndex, invocation.Arguments.IndexOf(actualArgument).ToString()) + .Add(ExpectedArgumentIndex, invocation.Arguments.IndexOf(expectedArgument).ToString()); + context.ReportDiagnostic(Diagnostic.Create( + PreferFunctionalAssert, + invocation.Syntax.GetLocation(), + properties, + messageArgs: [innerInvocation.TargetMethod.ContainingType.Name, innerInvocation.TargetMethod.Name])); + } + }; +} diff --git a/Funcky.Analyzers/Funcky.Analyzers/FunctionalAssert/FunctionalAssertMatching.cs b/Funcky.Analyzers/Funcky.Analyzers/FunctionalAssert/FunctionalAssertMatching.cs new file mode 100644 index 00000000..1efd7fed --- /dev/null +++ b/Funcky.Analyzers/Funcky.Analyzers/FunctionalAssert/FunctionalAssertMatching.cs @@ -0,0 +1,18 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Operations; + +namespace Funcky.Analyzers.FunctionalAssert; + +public sealed class FunctionalAssertMatching +{ + public static bool IsAssertMethodWithAccompanyingEqualOverload( + IInvocationOperation invocation, + AssertMethodHasOverloadWithExpectedValueAttributeType attributeType) + => invocation.TargetMethod.GetAttributes().Any(IsAttribute(attributeType.Value)) + && invocation.Arguments.Length == 1; + + private static Func IsAttribute(INamedTypeSymbol attributeType) + => data => SymbolEqualityComparer.Default.Equals(data.AttributeClass, attributeType); +} + +public sealed record AssertMethodHasOverloadWithExpectedValueAttributeType(INamedTypeSymbol Value); diff --git a/Funcky.Analyzers/Funcky.Analyzers/FunctionalAssert/XunitAssertMatching.cs b/Funcky.Analyzers/Funcky.Analyzers/FunctionalAssert/XunitAssertMatching.cs new file mode 100644 index 00000000..4eb44a24 --- /dev/null +++ b/Funcky.Analyzers/Funcky.Analyzers/FunctionalAssert/XunitAssertMatching.cs @@ -0,0 +1,29 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Operations; +using static Funcky.Analyzers.FunckyWellKnownMemberNames; + +namespace Funcky.Analyzers.FunctionalAssert; + +internal sealed class XunitAssertMatching +{ + public static bool MatchGenericAssertEqualInvocation( + IInvocationOperation invocation, + [NotNullWhen(true)] out IArgumentOperation? expectedArgument, + [NotNullWhen(true)] out IArgumentOperation? actualArgument) + { + const int expectedParameterIndex = 0; + const int actualParameterIndex = 1; + expectedArgument = null; + actualArgument = null; + return invocation.TargetMethod.Name == XunitAssert.EqualMethodName + && invocation.SemanticModel?.Compilation.GetXunitAssertType() is { } assertType + && SymbolEqualityComparer.Default.Equals(invocation.TargetMethod.ContainingType, assertType) + && invocation.TargetMethod.TypeParameters is [var typeParameter] + && invocation.TargetMethod.OriginalDefinition.Parameters is [var firstParameter, var secondParameter] + && SymbolEqualityComparer.Default.Equals(firstParameter.Type, typeParameter) + && SymbolEqualityComparer.Default.Equals(secondParameter.Type, typeParameter) + && (expectedArgument = invocation.GetArgumentForParameterAtIndex(expectedParameterIndex)) is var _ + && (actualArgument = invocation.GetArgumentForParameterAtIndex(actualParameterIndex)) is var _; + } +} diff --git a/Funcky.Test/Extensions/EnumerableExtensions/ElementAtOrNoneTest.cs b/Funcky.Test/Extensions/EnumerableExtensions/ElementAtOrNoneTest.cs index 260e17d0..13832df2 100644 --- a/Funcky.Test/Extensions/EnumerableExtensions/ElementAtOrNoneTest.cs +++ b/Funcky.Test/Extensions/EnumerableExtensions/ElementAtOrNoneTest.cs @@ -20,11 +20,11 @@ public void GivenANonEmptySequenceElementAtOrNoneReturnsSomeIfItsInTheRageOtherw FunctionalAssert.None(range.ElementAtOrNone(-42)); FunctionalAssert.None(range.ElementAtOrNone(-1)); - Assert.Equal(1, FunctionalAssert.Some(range.ElementAtOrNone(0))); - Assert.Equal(2, FunctionalAssert.Some(range.ElementAtOrNone(1))); - Assert.Equal(3, FunctionalAssert.Some(range.ElementAtOrNone(2))); - Assert.Equal(4, FunctionalAssert.Some(range.ElementAtOrNone(3))); - Assert.Equal(5, FunctionalAssert.Some(range.ElementAtOrNone(4))); + FunctionalAssert.Some(1, range.ElementAtOrNone(0)); + FunctionalAssert.Some(2, range.ElementAtOrNone(1)); + FunctionalAssert.Some(3, range.ElementAtOrNone(2)); + FunctionalAssert.Some(4, range.ElementAtOrNone(3)); + FunctionalAssert.Some(5, range.ElementAtOrNone(4)); FunctionalAssert.None(range.ElementAtOrNone(5)); FunctionalAssert.None(range.ElementAtOrNone(42)); FunctionalAssert.None(range.ElementAtOrNone(1337)); diff --git a/Funcky.Test/Extensions/EnumerableExtensions/GetNonEnumeratedCountOrNoneTest.cs b/Funcky.Test/Extensions/EnumerableExtensions/GetNonEnumeratedCountOrNoneTest.cs index c8a4d705..c0526aa1 100644 --- a/Funcky.Test/Extensions/EnumerableExtensions/GetNonEnumeratedCountOrNoneTest.cs +++ b/Funcky.Test/Extensions/EnumerableExtensions/GetNonEnumeratedCountOrNoneTest.cs @@ -23,7 +23,7 @@ public void GetNonEnumeratedCountOrNoneReturnsCountOnEnumerableRange() var range = Enumerable.Range(1, count); - Assert.Equal(count, FunctionalAssert.Some(range.GetNonEnumeratedCountOrNone())); + FunctionalAssert.Some(count, range.GetNonEnumeratedCountOrNone()); } [Property] diff --git a/Funcky.Test/Monads/OptionTest.cs b/Funcky.Test/Monads/OptionTest.cs index 6355645e..3f204847 100644 --- a/Funcky.Test/Monads/OptionTest.cs +++ b/Funcky.Test/Monads/OptionTest.cs @@ -226,10 +226,10 @@ public void GivenAnOptionAndAndAFuncToOptionItShouldBeFlattened() var some = Option.Some(42); FunctionalAssert.None(none.AndThen(_ => 1337)); - Assert.Equal(1337, FunctionalAssert.Some(some.AndThen(_ => 1337))); + FunctionalAssert.Some(1337, some.AndThen(_ => 1337)); FunctionalAssert.None(none.AndThen(_ => Option.Some(1337))); - Assert.Equal(1337, FunctionalAssert.Some(some.AndThen(_ => Option.Some(1337)))); + FunctionalAssert.Some(1337, some.AndThen(_ => Option.Some(1337))); } [Fact] diff --git a/Funcky.Xunit/CodeAnalysis/AssertMethodHasOverloadWithExpectedValueAttribute.cs b/Funcky.Xunit/CodeAnalysis/AssertMethodHasOverloadWithExpectedValueAttribute.cs new file mode 100644 index 00000000..641839ba --- /dev/null +++ b/Funcky.Xunit/CodeAnalysis/AssertMethodHasOverloadWithExpectedValueAttribute.cs @@ -0,0 +1,6 @@ +namespace Funcky.CodeAnalysis; + +/// Marks FunctionalAssert methods that have an accompanying overload that +/// takes the expected value. +[AttributeUsage(AttributeTargets.Method)] +internal sealed class AssertMethodHasOverloadWithExpectedValueAttribute : Attribute; diff --git a/Funcky.Xunit/FunctionalAssert/Left.cs b/Funcky.Xunit/FunctionalAssert/Left.cs index 561a3477..ad0902af 100644 --- a/Funcky.Xunit/FunctionalAssert/Left.cs +++ b/Funcky.Xunit/FunctionalAssert/Left.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using Funcky.CodeAnalysis; using Xunit.Sdk; using static Xunit.Sdk.ArgumentFormatter; @@ -41,6 +42,7 @@ public static void Left(TLeft expectedLeft, Either /// Asserts that the given is Left. /// Thrown when is Right. /// Returns the value in if it was Left. + [AssertMethodHasOverloadWithExpectedValue] #if STACK_TRACE_HIDDEN_SUPPORTED [System.Diagnostics.StackTraceHidden] #else diff --git a/Funcky.Xunit/FunctionalAssert/Ok.cs b/Funcky.Xunit/FunctionalAssert/Ok.cs index 5a040c64..66d6e71a 100644 --- a/Funcky.Xunit/FunctionalAssert/Ok.cs +++ b/Funcky.Xunit/FunctionalAssert/Ok.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using Funcky.CodeAnalysis; using Xunit.Sdk; using static Xunit.Sdk.ArgumentFormatter; @@ -40,6 +41,7 @@ public static void Ok(TValidResult expectedResult, ResultAsserts that the given is Ok. /// Thrown when is Error. /// Returns the value in if it was Ok. + [AssertMethodHasOverloadWithExpectedValue] #if STACK_TRACE_HIDDEN_SUPPORTED [System.Diagnostics.StackTraceHidden] #else diff --git a/Funcky.Xunit/FunctionalAssert/Right.cs b/Funcky.Xunit/FunctionalAssert/Right.cs index a0fc2871..efecf4b4 100644 --- a/Funcky.Xunit/FunctionalAssert/Right.cs +++ b/Funcky.Xunit/FunctionalAssert/Right.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using Funcky.CodeAnalysis; using Xunit.Sdk; using static Xunit.Sdk.ArgumentFormatter; @@ -41,6 +42,7 @@ public static void Right(TRight expectedRight, EitherAsserts that the given is Right. /// Thrown when is Left. /// Returns the value in if it was Right. + [AssertMethodHasOverloadWithExpectedValue] #if STACK_TRACE_HIDDEN_SUPPORTED [System.Diagnostics.StackTraceHidden] #else diff --git a/Funcky.Xunit/FunctionalAssert/Some.cs b/Funcky.Xunit/FunctionalAssert/Some.cs index c3533455..55a7aa0d 100644 --- a/Funcky.Xunit/FunctionalAssert/Some.cs +++ b/Funcky.Xunit/FunctionalAssert/Some.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using Funcky.CodeAnalysis; using Xunit.Sdk; using static Xunit.Sdk.ArgumentFormatter; @@ -40,6 +41,7 @@ public static void Some(TItem expectedValue, Option option) /// Asserts that the given is Some. /// Thrown when is None. /// Returns the value in if it was Some. + [AssertMethodHasOverloadWithExpectedValue] [Pure] #if STACK_TRACE_HIDDEN_SUPPORTED [System.Diagnostics.StackTraceHidden]