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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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|ℹ️|✔️|✔️|

<!-- rules -->

Expand Down
7 changes: 7 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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|<span title='Info'>ℹ️</span>|✔️|✔️|
|[MA0191](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0191.md)|Design|Do not use the null-forgiving operator|<span title='Warning'>⚠️</span>|❌|❌|
|[MA0192](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0192.md)|Usage|Use HasFlag instead of bitwise checks|<span title='Info'>ℹ️</span>|❌|✔️|
|[MA0193](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0193.md)|Usage|Use an overload with a MidpointRounding argument|<span title='Info'>ℹ️</span>|✔️|✔️|

|Id|Suppressed rule|Justification|
|--|---------------|-------------|
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
```
42 changes: 42 additions & 0 deletions docs/Rules/MA0193.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# MA0193 - Use an overload with a MidpointRounding argument
<!-- sources -->
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)
<!-- sources -->

`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<TSelf>.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);
}
}
````

Original file line number Diff line number Diff line change
@@ -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<string> 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<InvocationExpressionSyntax>();
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<IFieldSymbol>())
{
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<Document> 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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 3 additions & 0 deletions src/Meziantou.Analyzer.Pack/configuration/none.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions src/Meziantou.Analyzer/RuleIdentifiers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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<DiagnosticDescriptor> 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<IMethodSymbol>())
{
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;
}
}
Loading
Loading