diff --git a/src/xunit.analyzers.tests/Analyzers/X1000/InlineDataMustMatchTheoryParametersTests.cs b/src/xunit.analyzers.tests/Analyzers/X1000/InlineDataMustMatchTheoryParametersTests.cs index 7a2bba8d..be8339a0 100644 --- a/src/xunit.analyzers.tests/Analyzers/X1000/InlineDataMustMatchTheoryParametersTests.cs +++ b/src/xunit.analyzers.tests/Analyzers/X1000/InlineDataMustMatchTheoryParametersTests.cs @@ -1037,6 +1037,40 @@ public void TestMethod(T[] a) { } await Verify.VerifyAnalyzer(source); } + + [Theory] + [MemberData(nameof(SignedIntAndUnsignedInt))] + public async Task FromNegativeInteger_ToUnsignedInteger( + string signedType, + string unsignedType) + { + var source = string.Format(/* lang=c#-test */ """ + public class TestClass {{ + [Xunit.Theory] + [Xunit.InlineData({{|#0:({0})-1|}})] + public void TestMethod({1} value) {{ }} + }} + """, signedType, unsignedType); + var expected = Verify.Diagnostic("xUnit1010").WithLocation(0).WithArguments("value", unsignedType); + + await Verify.VerifyAnalyzer(source, expected); + } + + [Theory] + [MemberData(nameof(UnsignedIntegralTypes))] + public async Task FromLongMinValue_ToUnsignedInteger(string unsignedType) + { + var source = string.Format(/* lang=c#-test */ """ + public class TestClass {{ + [Xunit.Theory] + [Xunit.InlineData({{|#0:long.MinValue|}})] + public void TestMethod({0} value) {{ }} + }} + """, unsignedType); + var expected = Verify.Diagnostic("xUnit1010").WithLocation(0).WithArguments("value", unsignedType); + + await Verify.VerifyAnalyzer(source, expected); + } } public class DateTimeLikeParameter : X1010_IncompatibleValueType @@ -1252,6 +1286,18 @@ public class Explicit { public static IEnumerable> ValueTypedValues = IntegerValues.Concat(FloatingPointValues).Concat(BoolValues).Append(new("typeof(int)")); + + public static IEnumerable> SignedIntegralTypes = + ["int", "long", "short", "sbyte"]; + + public static IEnumerable> UnsignedIntegralTypes = + ["uint", "ulong", "ushort", "byte"]; + + public static readonly MatrixTheoryData SignedIntAndUnsignedInt = + new( + SignedIntegralTypes.Select(r => r.Data), + UnsignedIntegralTypes.Select(r => r.Data) + ); } public class X1011_ExtraValue diff --git a/src/xunit.analyzers/Utility/ConversionChecker.cs b/src/xunit.analyzers/Utility/ConversionChecker.cs index 8169a76d..47aa700a 100644 --- a/src/xunit.analyzers/Utility/ConversionChecker.cs +++ b/src/xunit.analyzers/Utility/ConversionChecker.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Globalization; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -7,11 +9,26 @@ namespace Xunit.Analyzers; static class ConversionChecker { + static readonly HashSet SignedIntegralTypes = [ + SpecialType.System_SByte, + SpecialType.System_Int16, + SpecialType.System_Int32, + SpecialType.System_Int64, + ]; + + static readonly HashSet UnsignedIntegralTypes = [ + SpecialType.System_Byte, + SpecialType.System_UInt16, + SpecialType.System_UInt32, + SpecialType.System_UInt64, + ]; + public static bool IsConvertible( Compilation compilation, ITypeSymbol source, ITypeSymbol destination, - XunitContext xunitContext) + XunitContext xunitContext, + object? valueSource = null) { Guard.ArgumentNotNull(compilation); Guard.ArgumentNotNull(source); @@ -32,7 +49,7 @@ public static bool IsConvertible( var conversion = compilation.ClassifyConversion(source, destination); if (conversion.IsNumeric) - return IsConvertibleNumeric(source, destination); + return IsConvertibleNumeric(source, destination, valueSource); if (destination.SpecialType == SpecialType.System_DateTime || (xunitContext.Core.TheorySupportsConversionFromStringToDateTimeOffsetAndGuid == true && IsDateTimeOffsetOrGuid(destination))) @@ -62,8 +79,13 @@ static bool IsConvertibleTypeParameter( static bool IsConvertibleNumeric( ITypeSymbol source, - ITypeSymbol destination) + ITypeSymbol destination, + object? valueSource = null) { + var isIntegral = long.TryParse(valueSource?.ToString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var integralValue); + if (isIntegral && integralValue < 0 && IsSigned(source) && IsUnsigned(destination)) + return false; + if (destination.SpecialType == SpecialType.System_Char && (source.SpecialType == SpecialType.System_Double || source.SpecialType == SpecialType.System_Single)) { @@ -81,4 +103,10 @@ static bool IsDateTimeOffsetOrGuid(ITypeSymbol destination) return destination.MetadataName == nameof(DateTimeOffset) || destination.MetadataName == nameof(Guid); } + + static bool IsSigned(ITypeSymbol typeSymbol) => + SignedIntegralTypes.Contains(typeSymbol.SpecialType); + + static bool IsUnsigned(ITypeSymbol typeSymbol) => + UnsignedIntegralTypes.Contains(typeSymbol.SpecialType); } diff --git a/src/xunit.analyzers/X1000/InlineDataMustMatchTheoryParameters.cs b/src/xunit.analyzers/X1000/InlineDataMustMatchTheoryParameters.cs index ba1e656b..3b3c0324 100644 --- a/src/xunit.analyzers/X1000/InlineDataMustMatchTheoryParameters.cs +++ b/src/xunit.analyzers/X1000/InlineDataMustMatchTheoryParameters.cs @@ -171,9 +171,9 @@ paramsElementType is not null if (value.Type is null) continue; - var isCompatible = ConversionChecker.IsConvertible(compilation, value.Type, parameter.Type, xunitContext); + var isCompatible = ConversionChecker.IsConvertible(compilation, value.Type, parameter.Type, xunitContext, value.Kind == TypedConstantKind.Primitive ? value.Value : null); if (!isCompatible && paramsElementType is not null) - isCompatible = ConversionChecker.IsConvertible(compilation, value.Type, paramsElementType, xunitContext); + isCompatible = ConversionChecker.IsConvertible(compilation, value.Type, paramsElementType, xunitContext, value.Kind == TypedConstantKind.Primitive ? value.Value : null); if (!isCompatible) {