diff --git a/README.md b/README.md index 5280f08d..0c34e061 100755 --- a/README.md +++ b/README.md @@ -211,6 +211,7 @@ If you are already using other analyzers, you can check [which rules are duplica |[MA0190](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0190.md)|Design|Use partial property instead of partial method for GeneratedRegex|ℹ️|✔️|✔️| |[MA0191](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0191.md)|Design|Do not use the null-forgiving operator|⚠️|❌|❌| |[MA0192](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0192.md)|Usage|Use HasFlag instead of bitwise checks|ℹ️|❌|✔️| +|[MA0193](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0193.md)|Usage|Use an overload with a MidpointRounding argument|ℹ️|✔️|✔️| diff --git a/docs/README.md b/docs/README.md index e0fd8a43..a78ce975 100755 --- a/docs/README.md +++ b/docs/README.md @@ -191,6 +191,7 @@ |[MA0190](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0190.md)|Design|Use partial property instead of partial method for GeneratedRegex|ℹ️|✔️|✔️| |[MA0191](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0191.md)|Design|Do not use the null-forgiving operator|⚠️|❌|❌| |[MA0192](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0192.md)|Usage|Use HasFlag instead of bitwise checks|ℹ️|❌|✔️| +|[MA0193](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0193.md)|Usage|Use an overload with a MidpointRounding argument|ℹ️|✔️|✔️| |Id|Suppressed rule|Justification| |--|---------------|-------------| @@ -779,6 +780,9 @@ dotnet_diagnostic.MA0191.severity = none # MA0192: Use HasFlag instead of bitwise checks dotnet_diagnostic.MA0192.severity = none + +# MA0193: Use an overload with a MidpointRounding argument +dotnet_diagnostic.MA0193.severity = suggestion ``` # .editorconfig - all rules disabled @@ -1353,4 +1357,7 @@ dotnet_diagnostic.MA0191.severity = none # MA0192: Use HasFlag instead of bitwise checks dotnet_diagnostic.MA0192.severity = none + +# MA0193: Use an overload with a MidpointRounding argument +dotnet_diagnostic.MA0193.severity = none ``` diff --git a/docs/Rules/MA0193.md b/docs/Rules/MA0193.md new file mode 100644 index 00000000..f2c81037 --- /dev/null +++ b/docs/Rules/MA0193.md @@ -0,0 +1,42 @@ +# MA0193 - Use an overload with a MidpointRounding argument + +Sources: [UseAnOverloadThatHasMidpointRoundingAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/UseAnOverloadThatHasMidpointRoundingAnalyzer.cs), [UseAnOverloadThatHasMidpointRoundingFixer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer.CodeFixers/Rules/UseAnOverloadThatHasMidpointRoundingFixer.cs) + + +`Round` overloads without a `MidpointRounding` argument use the default midpoint behavior (`ToEven`), which can be surprising. Prefer an overload that specifies the rounding mode explicitly. + +This rule reports calls to: + +- `Math.Round(...)` +- `MathF.Round(...)` +- `decimal.Round(...)` +- `IFloatingPoint.Round(...)` and implementations of those members + +## Non-compliant code + +````csharp +class Sample +{ + void M(decimal value) + { + _ = Math.Round(2.5); + _ = MathF.Round(2.5f); + _ = decimal.Round(value, 2); + } +} +```` + +## Compliant code + +````csharp +class Sample +{ + void M(decimal value) + { + _ = Math.Round(2.5, MidpointRounding.AwayFromZero); + _ = MathF.Round(2.5f, MidpointRounding.AwayFromZero); + _ = decimal.Round(value, 2, MidpointRounding.AwayFromZero); + } +} +```` + diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/UseAnOverloadThatHasMidpointRoundingFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/UseAnOverloadThatHasMidpointRoundingFixer.cs new file mode 100644 index 00000000..9a0f71e5 --- /dev/null +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/UseAnOverloadThatHasMidpointRoundingFixer.cs @@ -0,0 +1,118 @@ +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using Meziantou.Analyzer.Internals; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Operations; + +namespace Meziantou.Analyzer.Rules; + +[ExportCodeFixProvider(LanguageNames.CSharp), Shared] +public sealed class UseAnOverloadThatHasMidpointRoundingFixer : CodeFixProvider +{ + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(RuleIdentifiers.UseAnOverloadThatHasMidpointRounding); + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + var nodeToFix = root?.FindNode(context.Span, getInnermostNodeForTie: true); + if (nodeToFix is null) + return; + + var invocationExpression = nodeToFix as InvocationExpressionSyntax ?? nodeToFix.FirstAncestorOrSelf(); + if (invocationExpression is null) + return; + + var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); + if (semanticModel is null) + return; + + if (semanticModel.GetOperation(invocationExpression, context.CancellationToken) is not IInvocationOperation invocationOperation) + return; + + var midpointRoundingSymbol = semanticModel.Compilation.GetBestTypeByMetadataName("System.MidpointRounding"); + if (midpointRoundingSymbol is null) + return; + + if (!TryGetMidpointRoundingParameterInfo(semanticModel.Compilation, invocationOperation, midpointRoundingSymbol, out var parameterInfo)) + return; + + foreach (var midpointRoundingMember in midpointRoundingSymbol.GetMembers().OfType()) + { + if (midpointRoundingMember is { IsImplicitlyDeclared: true, Name: "value__" }) + continue; + + if (!midpointRoundingMember.HasConstantValue) + continue; + + var midpointRoundingMemberName = midpointRoundingMember.Name; + var title = "Add MidpointRounding." + midpointRoundingMemberName; + var codeAction = CodeAction.Create( + title, + ct => AddMidpointRounding(context.Document, invocationExpression, parameterInfo, midpointRoundingSymbol, midpointRoundingMemberName, ct), + equivalenceKey: title); + + context.RegisterCodeFix(codeAction, context.Diagnostics); + } + } + + private static bool TryGetMidpointRoundingParameterInfo(Compilation compilation, IInvocationOperation invocationOperation, INamedTypeSymbol midpointRoundingSymbol, out AdditionalParameterInfo parameterInfo) + { + var overloadFinder = new OverloadFinder(compilation); + var overload = overloadFinder.FindOverloadWithAdditionalParameterOfType(invocationOperation, new OverloadOptions(IncludeObsoleteMembers: false, AllowOptionalParameters: true), [midpointRoundingSymbol]); + if (overload is null) + { + parameterInfo = default; + return false; + } + + for (var i = 0; i < overload.Parameters.Length; i++) + { + if (overload.Parameters[i].Type.IsEqualTo(midpointRoundingSymbol)) + { + parameterInfo = new AdditionalParameterInfo(i, overload.Parameters[i].Name); + return true; + } + } + + parameterInfo = default; + return false; + } + + private static async Task AddMidpointRounding(Document document, InvocationExpressionSyntax invocationExpression, AdditionalParameterInfo parameterInfo, INamedTypeSymbol midpointRoundingSymbol, string midpointRoundingMember, CancellationToken cancellationToken) + { + var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); + var generator = editor.Generator; + + var midpointRoundingExpression = generator.MemberAccessExpression( + generator.TypeExpression(midpointRoundingSymbol, addImport: true), + midpointRoundingMember); + + var newArgument = (ArgumentSyntax)generator.Argument(midpointRoundingExpression); + + InvocationExpressionSyntax newInvocation; + if (parameterInfo.ParameterIndex > invocationExpression.ArgumentList.Arguments.Count) + { + var namedArgument = (ArgumentSyntax)generator.Argument(parameterInfo.ParameterName, RefKind.None, midpointRoundingExpression); + var newArguments = invocationExpression.ArgumentList.Arguments.Add(namedArgument); + newInvocation = invocationExpression.WithArgumentList(SyntaxFactory.ArgumentList(newArguments)); + } + else + { + var newArguments = invocationExpression.ArgumentList.Arguments.Insert(parameterInfo.ParameterIndex, newArgument); + newInvocation = invocationExpression.WithArgumentList(SyntaxFactory.ArgumentList(newArguments)); + } + + editor.ReplaceNode(invocationExpression, newInvocation); + return editor.GetChangedDocument(); + } + + private readonly record struct AdditionalParameterInfo(int ParameterIndex, string? ParameterName); +} diff --git a/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig b/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig index 22060bd0..23767b0f 100644 --- a/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig +++ b/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig @@ -571,3 +571,6 @@ dotnet_diagnostic.MA0191.severity = none # MA0192: Use HasFlag instead of bitwise checks dotnet_diagnostic.MA0192.severity = none + +# MA0193: Use an overload with a MidpointRounding argument +dotnet_diagnostic.MA0193.severity = suggestion diff --git a/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig b/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig index 27bd6318..ff3c0443 100644 --- a/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig +++ b/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig @@ -571,3 +571,6 @@ dotnet_diagnostic.MA0191.severity = none # MA0192: Use HasFlag instead of bitwise checks dotnet_diagnostic.MA0192.severity = none + +# MA0193: Use an overload with a MidpointRounding argument +dotnet_diagnostic.MA0193.severity = none diff --git a/src/Meziantou.Analyzer/RuleIdentifiers.cs b/src/Meziantou.Analyzer/RuleIdentifiers.cs index f94bd899..e99b2981 100755 --- a/src/Meziantou.Analyzer/RuleIdentifiers.cs +++ b/src/Meziantou.Analyzer/RuleIdentifiers.cs @@ -192,6 +192,7 @@ internal static class RuleIdentifiers public const string UsePartialPropertyInsteadOfPartialMethodForGeneratedRegex = "MA0190"; public const string DoNotUseNullForgiveness = "MA0191"; public const string UseHasFlagMethod = "MA0192"; + public const string UseAnOverloadThatHasMidpointRounding = "MA0193"; public static string GetHelpUri(string identifier) { diff --git a/src/Meziantou.Analyzer/Rules/UseAnOverloadThatHasMidpointRoundingAnalyzer.cs b/src/Meziantou.Analyzer/Rules/UseAnOverloadThatHasMidpointRoundingAnalyzer.cs new file mode 100644 index 00000000..3c42d636 --- /dev/null +++ b/src/Meziantou.Analyzer/Rules/UseAnOverloadThatHasMidpointRoundingAnalyzer.cs @@ -0,0 +1,121 @@ +using System.Collections.Immutable; +using System.Linq; +using Meziantou.Analyzer.Internals; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Meziantou.Analyzer.Rules; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class UseAnOverloadThatHasMidpointRoundingAnalyzer : DiagnosticAnalyzer +{ + private static readonly DiagnosticDescriptor Rule = new( + RuleIdentifiers.UseAnOverloadThatHasMidpointRounding, + title: "Use an overload with a MidpointRounding argument", + messageFormat: "Use an overload with a MidpointRounding argument", + RuleCategories.Usage, + DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: "", + helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.UseAnOverloadThatHasMidpointRounding)); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + + context.RegisterCompilationStartAction(context => + { + var midpointRoundingSymbol = context.Compilation.GetBestTypeByMetadataName("System.MidpointRounding"); + if (midpointRoundingSymbol is null) + return; + + var ifloatingPointSymbol = context.Compilation.GetBestTypeByMetadataName("System.Numerics.IFloatingPoint`1"); + var mathSymbol = context.Compilation.GetBestTypeByMetadataName("System.Math"); + var mathFSymbol = context.Compilation.GetBestTypeByMetadataName("System.MathF"); + if (ifloatingPointSymbol is null && mathSymbol is null && mathFSymbol is null) + return; + + context.RegisterOperationAction(context => AnalyzeInvocation(context, midpointRoundingSymbol, ifloatingPointSymbol, mathSymbol, mathFSymbol), OperationKind.Invocation); + }); + } + + private static void AnalyzeInvocation( + OperationAnalysisContext context, + INamedTypeSymbol midpointRoundingSymbol, + INamedTypeSymbol? ifloatingPointSymbol, + INamedTypeSymbol? mathSymbol, + INamedTypeSymbol? mathFSymbol) + { + var operation = (IInvocationOperation)context.Operation; + var method = operation.TargetMethod; + if (!IsRoundMethodWithoutMidpointRounding(method, midpointRoundingSymbol)) + return; + + if (method.ContainingType.IsEqualTo(mathSymbol) || method.ContainingType.IsEqualTo(mathFSymbol)) + { + context.ReportDiagnostic(Rule, operation); + return; + } + + if (method.ContainingType.SpecialType is SpecialType.System_Decimal) + { + context.ReportDiagnostic(Rule, operation); + return; + } + + if (IsIFloatingPointRoundMethod(method, midpointRoundingSymbol, ifloatingPointSymbol) || + IsIFloatingPointRoundImplementation(method, midpointRoundingSymbol, ifloatingPointSymbol)) + { + context.ReportDiagnostic(Rule, operation); + } + } + + private static bool IsRoundMethodWithoutMidpointRounding(IMethodSymbol method, INamedTypeSymbol midpointRoundingSymbol) + { + return method.Name is "Round" && + !method.Parameters.Any(parameter => parameter.Type.IsEqualTo(midpointRoundingSymbol)); + } + + private static bool IsIFloatingPointRoundMethod(IMethodSymbol method, INamedTypeSymbol midpointRoundingSymbol, INamedTypeSymbol? ifloatingPointSymbol) + { + if (ifloatingPointSymbol is null) + return false; + + return IsRoundMethodWithoutMidpointRounding(method, midpointRoundingSymbol) && + method.ContainingType.OriginalDefinition.IsEqualTo(ifloatingPointSymbol); + } + + private static bool IsIFloatingPointRoundImplementation(IMethodSymbol method, INamedTypeSymbol midpointRoundingSymbol, INamedTypeSymbol? ifloatingPointSymbol) + { + if (ifloatingPointSymbol is null || method.ContainingType is null) + return false; + + foreach (var explicitImplementation in method.ExplicitInterfaceImplementations) + { + if (IsIFloatingPointRoundMethod(explicitImplementation, midpointRoundingSymbol, ifloatingPointSymbol)) + return true; + } + + foreach (var interfaceType in method.ContainingType.AllInterfaces) + { + if (!interfaceType.OriginalDefinition.IsEqualTo(ifloatingPointSymbol)) + continue; + + foreach (var interfaceMethod in interfaceType.GetMembers(method.Name).OfType()) + { + if (!IsIFloatingPointRoundMethod(interfaceMethod, midpointRoundingSymbol, ifloatingPointSymbol)) + continue; + + var implementation = method.ContainingType.FindImplementationForInterfaceMember(interfaceMethod); + if (implementation is IMethodSymbol implementationMethod && implementationMethod.OriginalDefinition.IsEqualTo(method.OriginalDefinition)) + return true; + } + } + + return false; + } +} diff --git a/tests/Meziantou.Analyzer.Test/Rules/UseAnOverloadThatHasMidpointRoundingAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/UseAnOverloadThatHasMidpointRoundingAnalyzerTests.cs new file mode 100644 index 00000000..4c41087e --- /dev/null +++ b/tests/Meziantou.Analyzer.Test/Rules/UseAnOverloadThatHasMidpointRoundingAnalyzerTests.cs @@ -0,0 +1,271 @@ +using Meziantou.Analyzer.Rules; +using Meziantou.Analyzer.Test.Helpers; +using TestHelper; + +namespace Meziantou.Analyzer.Test.Rules; + +public sealed class UseAnOverloadThatHasMidpointRoundingAnalyzerTests +{ + private static ProjectBuilder CreateProjectBuilder() + { + return new ProjectBuilder() + .WithAnalyzer() + .WithCodeFixProvider(); + } + + [Fact] + public async Task MathRoundWithoutMode_ReportDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" + class Test + { + void A() + { + _ = [|System.Math.Round(2.5)|]; + _ = [|System.Math.Round(2.5, 1)|]; + } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task MathRoundWithMode_NoDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" + class Test + { + void A() + { + _ = System.Math.Round(2.5, System.MidpointRounding.AwayFromZero); + _ = System.Math.Round(2.5, 1, System.MidpointRounding.AwayFromZero); + } + } + """) + .ValidateAsync(); + } + + [Theory] + [InlineData(0, "ToEven")] + [InlineData(1, "AwayFromZero")] + [InlineData(2, "ToZero")] + [InlineData(3, "ToNegativeInfinity")] + [InlineData(4, "ToPositiveInfinity")] + public async Task MathRound_CodeFix_SuggestsEachMidpointRoundingValue(int codeFixIndex, string midpointRoundingMember) + { + var fixedCode = $$""" + class Test + { + void A() + { + _ = System.Math.Round(2.5, System.MidpointRounding.{{midpointRoundingMember}}); + } + } + """; + + await CreateProjectBuilder() + .WithSourceCode(""" + class Test + { + void A() + { + _ = [|System.Math.Round(2.5)|]; + } + } + """) + .ShouldFixCodeWith(codeFixIndex, fixedCode) + .ValidateAsync(); + } + + [Fact] + public async Task MathFRoundWithoutMode_ReportDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" + class Test + { + void A() + { + _ = [|System.MathF.Round(2.5f)|]; + _ = [|System.MathF.Round(2.5f, 1)|]; + } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task MathFRoundWithMode_NoDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" + class Test + { + void A() + { + _ = System.MathF.Round(2.5f, System.MidpointRounding.AwayFromZero); + _ = System.MathF.Round(2.5f, 1, System.MidpointRounding.AwayFromZero); + } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task DecimalRoundWithoutMode_ReportDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" + class Test + { + void A(decimal value) + { + _ = [|decimal.Round(value)|]; + _ = [|decimal.Round(value, 1)|]; + } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task DecimalRoundWithMode_NoDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" + class Test + { + void A(decimal value) + { + _ = decimal.Round(value, System.MidpointRounding.AwayFromZero); + _ = decimal.Round(value, 1, System.MidpointRounding.AwayFromZero); + } + } + """) + .ValidateAsync(); + } + + [Theory] + [InlineData(0, "ToEven")] + [InlineData(1, "AwayFromZero")] + [InlineData(2, "ToZero")] + [InlineData(3, "ToNegativeInfinity")] + [InlineData(4, "ToPositiveInfinity")] + public async Task DecimalRound_CodeFix_SuggestsEachMidpointRoundingValue(int codeFixIndex, string midpointRoundingMember) + { + var fixedCode = $$""" + class Test + { + void A(decimal value) + { + _ = decimal.Round(value, 1, System.MidpointRounding.{{midpointRoundingMember}}); + } + } + """; + + await CreateProjectBuilder() + .WithSourceCode(""" + class Test + { + void A(decimal value) + { + _ = [|decimal.Round(value, 1)|]; + } + } + """) + .ShouldFixCodeWith(codeFixIndex, fixedCode) + .ValidateAsync(); + } + +#if CSHARP11_OR_GREATER + [Fact] + public async Task FloatingPointImplementationsRoundWithoutMode_ReportDiagnostic() + { + await CreateProjectBuilder() + .WithTargetFramework(TargetFramework.Net7_0) + .WithLanguageVersion(Microsoft.CodeAnalysis.CSharp.LanguageVersion.CSharp11) + .WithSourceCode(""" + class Test + { + void A(double d, float f, System.Half h) + { + _ = [|double.Round(d)|]; + _ = [|double.Round(d, 1)|]; + _ = [|float.Round(f)|]; + _ = [|float.Round(f, 1)|]; + _ = [|System.Half.Round(h)|]; + _ = [|System.Half.Round(h, 1)|]; + } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task FloatingPointImplementationsRoundWithMode_NoDiagnostic() + { + await CreateProjectBuilder() + .WithTargetFramework(TargetFramework.Net7_0) + .WithLanguageVersion(Microsoft.CodeAnalysis.CSharp.LanguageVersion.CSharp11) + .WithSourceCode(""" + class Test + { + void A(double d, float f, System.Half h) + { + _ = double.Round(d, System.MidpointRounding.AwayFromZero); + _ = double.Round(d, 1, System.MidpointRounding.AwayFromZero); + _ = float.Round(f, System.MidpointRounding.AwayFromZero); + _ = float.Round(f, 1, System.MidpointRounding.AwayFromZero); + _ = System.Half.Round(h, System.MidpointRounding.AwayFromZero); + _ = System.Half.Round(h, 1, System.MidpointRounding.AwayFromZero); + } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task IFloatingPointRoundWithoutMode_ReportDiagnostic() + { + await CreateProjectBuilder() + .WithTargetFramework(TargetFramework.Net7_0) + .WithLanguageVersion(Microsoft.CodeAnalysis.CSharp.LanguageVersion.CSharp11) + .WithSourceCode(""" + using System.Numerics; + + class Test + { + static T Round(T value) where T : IFloatingPoint + { + _ = [|T.Round(value)|]; + return [|T.Round(value, 1)|]; + } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task IFloatingPointRoundWithMode_NoDiagnostic() + { + await CreateProjectBuilder() + .WithTargetFramework(TargetFramework.Net7_0) + .WithLanguageVersion(Microsoft.CodeAnalysis.CSharp.LanguageVersion.CSharp11) + .WithSourceCode(""" + using System.Numerics; + + class Test + { + static T Round(T value) where T : IFloatingPoint + { + _ = T.Round(value, System.MidpointRounding.AwayFromZero); + return T.Round(value, 1, System.MidpointRounding.AwayFromZero); + } + } + """) + .ValidateAsync(); + } +#endif +}