Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
```
43 changes: 43 additions & 0 deletions docs/Rules/MA0193.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# 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);
}
}
````

The code fix suggests one action for each available `System.MidpointRounding` enum value.
Comment thread
meziantou marked this conversation as resolved.
Outdated
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