Add Analyzer to warn about decimal<=>float/double casts

fixes c882fe4ea and 32a66a955
This commit is contained in:
YoshiRulz 2024-09-17 00:53:20 +10:00
parent 6dbee180e9
commit 97a8e9011e
No known key found for this signature in database
GPG Key ID: C4DE31C245353FB7
10 changed files with 203 additions and 7 deletions

View File

@ -29,6 +29,8 @@ dotnet_diagnostic.BHI1102.severity = error
dotnet_diagnostic.BHI1103.severity = error
# Don't use ^= (XOR-assign) for inverting the value of booleans
dotnet_diagnostic.BHI1104.severity = error
# Use unambiguous decimal<=>float/double conversion methods
dotnet_diagnostic.BHI1105.severity = error
# Brackets of collection expression should be separated with spaces
dotnet_diagnostic.BHI1110.severity = warning
# Expression-bodied member should be flowed to next line correctly

View File

@ -0,0 +1,80 @@
namespace BizHawk.Analyzers;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class AmbiguousMoneyToFloatConversionAnalyzer : DiagnosticAnalyzer
{
private static readonly DiagnosticDescriptor DiagAmbiguousMoneyToFloatConversion = new(
id: "BHI1105",
title: "Use unambiguous decimal<=>float/double conversion methods",
messageFormat: "use {0} for checked conversion, or {1} for unchecked",
category: "Usage",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(DiagAmbiguousMoneyToFloatConversion);
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterCompilationStartAction(initContext =>
{
var decimalSym = initContext.Compilation.GetTypeByMetadataName("System.Decimal")!;
var doubleSym = initContext.Compilation.GetTypeByMetadataName("System.Double")!;
var floatSym = initContext.Compilation.GetTypeByMetadataName("System.Single")!;
initContext.RegisterOperationAction(oac =>
{
var conversionOp = (IConversionOperation) oac.Operation;
var typeOutput = conversionOp.Type;
var typeInput = conversionOp.Operand.Type;
bool isToDecimal;
bool isDoublePrecision;
if (decimalSym.Matches(typeOutput))
{
if (doubleSym.Matches(typeInput)) isDoublePrecision = true;
else if (floatSym.Matches(typeInput)) isDoublePrecision = false;
else return;
isToDecimal = true;
}
else if (decimalSym.Matches(typeInput))
{
if (doubleSym.Matches(typeOutput)) isDoublePrecision = true;
else if (floatSym.Matches(typeOutput)) isDoublePrecision = false;
else return;
isToDecimal = false;
}
else
{
return;
}
var conversionSyn = conversionOp.Syntax;
//TODO check the suggested methods are accessible (i.e. BizHawk.Common is referenced)
oac.ReportDiagnostic(Diagnostic.Create(
DiagAmbiguousMoneyToFloatConversion,
(conversionSyn.Parent?.Kind() is SyntaxKind.CheckedExpression or SyntaxKind.UncheckedExpression
? conversionSyn.Parent
: conversionSyn).GetLocation(),
conversionOp.IsChecked ? DiagnosticSeverity.Error : DiagnosticSeverity.Warning,
additionalLocations: null,
properties: null,
messageArgs: isToDecimal
? [
$"new decimal({(isDoublePrecision ? "double" : "float")})", // "checked"
"static NumberExtensions.ConvertToMoneyTruncated", // "unchecked"
]
: [
$"decimal.{(isDoublePrecision ? "ConvertToF64" : "ConvertToF32")} ext. (from NumberExtensions)", // "checked"
$"static Decimal.{(isDoublePrecision ? "ToDouble" : "ToSingle")}", // "unchecked"
]));
},
OperationKind.Conversion);
});
}
}

View File

@ -0,0 +1,32 @@
namespace BizHawk.Tests.Analyzers;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Verify = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<
BizHawk.Analyzers.AmbiguousMoneyToFloatConversionAnalyzer,
Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
[TestClass]
public sealed class AmbiguousMoneyToFloatConversionAnalyzerTests
{
[TestMethod]
public Task CheckMisuseOfDecimalExplicitCastOperators()
=> Verify.VerifyAnalyzerAsync("""
public static class Cases {
private static float Y(decimal m)
=> decimal.ToSingle(m);
private static decimal Z(double d)
=> new(d);
private static float A(decimal m)
=> {|BHI1105:unchecked((float) m)|};
private static decimal B(double d)
=> {|BHI1105:checked((decimal) d)|};
private static decimal C(float d)
=> {|BHI1105:unchecked((decimal) d)|};
private static double D(decimal m)
=> {|BHI1105:checked((double) m)|};
}
""");
}

View File

@ -1,7 +1,7 @@
<Project>
<Import Project="../Common.props" />
<PropertyGroup>
<NoWarn>$(NoWarn);MEN018;SA1200</NoWarn>
<NoWarn>$(NoWarn);BHI1105;MEN018;SA1200</NoWarn>
</PropertyGroup>
<ItemGroup>
<None Remove="*.sh" />

Binary file not shown.

View File

@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using BizHawk.Common.NumberExtensions;
using BizHawk.Common.StringExtensions;
using BizHawk.Emulation.Common;
@ -71,7 +72,7 @@ namespace BizHawk.Client.Common
const decimal attosInSec = 1_000_000_000_000_000_000.0M;
var m = attosInSec;
m /= ulong.Parse(vsyncAttoStr);
return checked((double) m);
return m.ConvertToF64();
}
return PlatformFrameRates.GetFrameRate(SystemID, IsPal);

View File

@ -1,3 +1,4 @@
using System.Numerics;
using System.Windows.Forms;
using BizHawk.Client.Common;
@ -57,7 +58,12 @@ namespace BizHawk.Client.EmuHawk
RewindEnabledBox.Checked = _config.Rewind.Enabled;
UseCompression.Checked = _config.Rewind.UseCompression;
cbDeltaCompression.Checked = _config.Rewind.UseDelta;
BufferSizeUpDown.Value = Math.Max((decimal) Math.Log(_config.Rewind.BufferSize, 2), BufferSizeUpDown.Minimum);
BufferSizeUpDown.Value = Math.Max(
BufferSizeUpDown.Minimum,
_config.Rewind.BufferSize < 0L
? 0.0M
: new decimal(BitOperations.Log2(unchecked((ulong) _config.Rewind.BufferSize)))
);
TargetFrameLengthRadioButton.Checked = !_config.Rewind.UseFixedRewindInterval;
TargetRewindIntervalRadioButton.Checked = _config.Rewind.UseFixedRewindInterval;
TargetFrameLengthNumeric.Value = Math.Max(_config.Rewind.TargetFrameLength, TargetFrameLengthNumeric.Minimum);

View File

@ -1,5 +1,7 @@
using System.Windows.Forms;
using BizHawk.Common.NumberExtensions;
namespace BizHawk.Client.EmuHawk
{
public partial class BotControlsRow : UserControl
@ -21,8 +23,8 @@ namespace BizHawk.Client.EmuHawk
public double Probability
{
get => (double)ProbabilityUpDown.Value;
set => ProbabilityUpDown.Value = (decimal)value;
get => ProbabilityUpDown.Value.ConvertToF64();
set => ProbabilityUpDown.Value = new(value);
}
private void ProbabilityUpDown_ValueChanged(object sender, EventArgs e)

View File

@ -1,6 +1,7 @@
using System.Drawing;
using System.Windows.Forms;
using BizHawk.Client.Common;
using BizHawk.Common.NumberExtensions;
using BizHawk.Emulation.Common;
namespace BizHawk.Client.EmuHawk
@ -195,7 +196,7 @@ namespace BizHawk.Client.EmuHawk
XNumeric.Value = XNumeric.Maximum;
}
_stickyXorAdapter.SetAxis(XName, (int)((float)XNumeric.Value * MultiplierX));
_stickyXorAdapter.SetAxis(XName, (XNumeric.Value.ConvertToF32() * MultiplierX).RoundToInt());
_isSet = true;
}
}
@ -217,7 +218,7 @@ namespace BizHawk.Client.EmuHawk
YNumeric.Value = YNumeric.Maximum;
}
_stickyXorAdapter.SetAxis(YName, (int)((float)YNumeric.Value * MultiplierY));
_stickyXorAdapter.SetAxis(YName, (YNumeric.Value.ConvertToF32() * MultiplierY).RoundToInt());
_isSet = true;
}
}

View File

@ -5,6 +5,8 @@ namespace BizHawk.Common.NumberExtensions
{
public static class NumberExtensions
{
private const string ERR_MSG_PRECISION_LOSS = "unable to convert from decimal without loss of precision";
public static string ToHexString(this int n, int numDigits)
{
return string.Format($"{{0:X{numDigits}}}", n);
@ -55,6 +57,76 @@ namespace BizHawk.Common.NumberExtensions
return (byte)(((v / 16) * 10) + (v % 16));
}
/// <returns>the <see langword="float"/> whose value is closest to <paramref name="m"/></returns>
/// <exception cref="OverflowException">loss of precision (the value won't survive a round-trip)</exception>
/// <remarks>like a <c>checked</c> conversion</remarks>
public static float ConvertToF32(this decimal m)
{
var f = decimal.ToSingle(m);
return m.Equals(new decimal(f)) ? f : throw new OverflowException(ERR_MSG_PRECISION_LOSS);
}
/// <returns>the <see langword="double"/> whose value is closest to <paramref name="m"/></returns>
/// <exception cref="OverflowException">loss of precision (the value won't survive a round-trip)</exception>
/// <remarks>like a <c>checked</c> conversion</remarks>
public static double ConvertToF64(this decimal m)
{
var d = decimal.ToDouble(m);
return m.Equals(new decimal(d)) ? d : throw new OverflowException(ERR_MSG_PRECISION_LOSS);
}
/// <returns>the <see langword="decimal"/> whose value is closest to <paramref name="f"/></returns>
/// <exception cref="NotFiniteNumberException">
/// iff <paramref name="f"/> is NaN and <paramref name="throwIfNaN"/> is set
/// (infinite values are rounded to <see cref="decimal.MinValue"/>/<see cref="decimal.MaxValue"/>)
/// </exception>
/// <remarks>like an <c>unchecked</c> conversion</remarks>
public static decimal ConvertToMoneyTruncated(float f, bool throwIfNaN = false)
{
try
{
#pragma warning disable BHI1105 // this is the sanctioned call-site
return (decimal) f;
#pragma warning restore BHI1105
}
catch (OverflowException)
{
return float.IsNaN(f)
? throwIfNaN
? throw new NotFiniteNumberException(f)
: default
: f < 0.0f
? decimal.MinValue
: decimal.MaxValue;
}
}
/// <returns>the <see langword="decimal"/> whose value is closest to <paramref name="d"/></returns>
/// <exception cref="NotFiniteNumberException">
/// iff <paramref name="d"/> is NaN and <paramref name="throwIfNaN"/> is set
/// (infinite values are rounded to <see cref="decimal.MinValue"/>/<see cref="decimal.MaxValue"/>)
/// </exception>
/// <remarks>like an <c>unchecked</c> conversion</remarks>
public static decimal ConvertToMoneyTruncated(double d, bool throwIfNaN = false)
{
try
{
#pragma warning disable BHI1105 // this is the sanctioned call-site
return (decimal) d;
#pragma warning restore BHI1105
}
catch (OverflowException)
{
return double.IsNaN(d)
? throwIfNaN
? throw new NotFiniteNumberException(d)
: default
: d < 0.0
? decimal.MinValue
: decimal.MaxValue;
}
}
/// <summary>
/// Receives a number and returns the number of hexadecimal digits it is
/// Note: currently only returns 2, 4, 6, or 8