diff --git a/analyzers/src/SonarAnalyzer.Common/SymbolicExecution/Roslyn/OperationProcessors/Binary.Arithmetic.cs b/analyzers/src/SonarAnalyzer.Common/SymbolicExecution/Roslyn/OperationProcessors/Binary.Arithmetic.cs index 89b0db8f694..9721d3948db 100644 --- a/analyzers/src/SonarAnalyzer.Common/SymbolicExecution/Roslyn/OperationProcessors/Binary.Arithmetic.cs +++ b/analyzers/src/SonarAnalyzer.Common/SymbolicExecution/Roslyn/OperationProcessors/Binary.Arithmetic.cs @@ -35,6 +35,8 @@ internal sealed partial class Binary BinaryOperatorKind.And => NumberConstraint.From(CalculateAndMin(left, right), CalculateAndMax(left, right)), BinaryOperatorKind.Or when left.IsSingleValue && right.IsSingleValue => NumberConstraint.From(left.Min.Value | right.Min.Value), BinaryOperatorKind.Or => NumberConstraint.From(CalculateOrMin(left, right), CalculateOrMax(left, right)), + BinaryOperatorKind.ExclusiveOr when left.IsSingleValue && right.IsSingleValue => NumberConstraint.From(left.Min.Value ^ right.Min.Value), + BinaryOperatorKind.ExclusiveOr => NumberConstraint.From(CalculateXorMin(left, right), CalculateXorMax(left, right)), _ => null }; @@ -213,6 +215,81 @@ private static NumberConstraint AccountForZero(NumberConstraint constraint) } } + private static BigInteger? CalculateXorMin(NumberConstraint left, NumberConstraint right) + { + if (left.IsPositive && right.IsPositive) + { + return SameSign(left, right); + } + else if (left.IsNegative && right.IsNegative) + { + return SameSign(right, left); + } + // Positive numbers start with Zeroes. Negative numbers start with Ones. XOR them, and the result will start with Ones and thus will be negative. + // By taking a look at the number of starting Zeroes and Ones, we can also learn a limit for the number of starting Ones of the result. + // Note: When passing a positive limit to NegativeMagnitude, it needs to be increased by 1 and then multiplied by -1 to get the expected result. + else if ((left.IsPositive || right.IsNegative) && left.Max.HasValue && right.Min.HasValue) + { + return NegativeMagnitude(-BigInteger.Max(left.Max.Value + 1, BigInteger.Abs(right.Min.Value))); + } + else if ((left.IsNegative || right.IsPositive) && left.Min.HasValue && right.Max.HasValue) + { + return NegativeMagnitude(-BigInteger.Max(BigInteger.Abs(left.Min.Value), right.Max.Value + 1)); + } + else if (left.Min.HasValue && left.Max.HasValue && right.Min.HasValue && right.Max.HasValue) + { + return NegativeMagnitude(-Max(BigInteger.Abs(left.Min.Value), left.Max.Value + 1, BigInteger.Abs(right.Min.Value), right.Max.Value + 1)); + } + else + { + return null; + } + + static BigInteger? SameSign(NumberConstraint range1, NumberConstraint range2) + { + // Takes advantage of the property a - b <= a ^ b for all a >= 0 and b >= 0 + // If ranges overlap => at least 1 value belongs to both ranges => xor can yield 0 + if (range1.Min > range2.Max) + { + return range1.Min.Value - range2.Max.Value; + } + else if (range2.Min > range1.Max) + { + return range2.Min.Value - range1.Max.Value; + } + else + { + return 0; + } + } + } + + private static BigInteger? CalculateXorMax(NumberConstraint left, NumberConstraint right) + { + if ((left.IsPositive && right.IsNegative) || (left.IsNegative && right.IsPositive)) + { + return -1; + } + else if ((left.IsPositive || right.IsPositive) && left.Max.HasValue && right.Max.HasValue) + { + return PositiveMagnitude(BigInteger.Max(left.Max.Value, right.Max.Value)); + } + else if ((left.IsNegative || right.IsNegative) && left.Min.HasValue && right.Min.HasValue) + { + return PositiveMagnitude(BigInteger.Max(BigInteger.Abs(left.Min.Value), BigInteger.Abs(right.Min.Value))); + } + else if (left.Min.HasValue && left.Max.HasValue && right.Min.HasValue && right.Max.HasValue) + { + return PositiveMagnitude(Max(BigInteger.Abs(left.Min.Value), left.Max.Value, BigInteger.Abs(right.Min.Value), right.Max.Value)); + } + else + { + return null; + } + } + + private static BigInteger Max(params BigInteger[] values) => values.Max(); + private static BigInteger? NegativeMagnitude(BigInteger value) { // For increasing powers of 2 with negative sign, we're looking for the longest chain of 1 from the MSB side diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/SymbolicExecution/Roslyn/RoslynSymbolicExecutionTest.Binary.cs b/analyzers/tests/SonarAnalyzer.UnitTest/SymbolicExecution/Roslyn/RoslynSymbolicExecutionTest.Binary.cs index 03fc6d2c8ee..8edc1fab2b2 100644 --- a/analyzers/tests/SonarAnalyzer.UnitTest/SymbolicExecution/Roslyn/RoslynSymbolicExecutionTest.Binary.cs +++ b/analyzers/tests/SonarAnalyzer.UnitTest/SymbolicExecution/Roslyn/RoslynSymbolicExecutionTest.Binary.cs @@ -922,7 +922,7 @@ public void Binary_BitAnd_SingleValue(int left, int right, int expected) var value = left & right; Tag("Value", value); """; - SETestContext.CreateCS(code).Validator.ValidateTag("Value", x => x.Should().HaveOnlyConstraints(ObjectConstraint.NotNull, NumberConstraint.From(expected))); + SETestContext.CreateCS(code).Validator.TagValue("Value").Should().HaveOnlyConstraints(ObjectConstraint.NotNull, NumberConstraint.From(expected)); } [DataTestMethod] @@ -1029,4 +1029,79 @@ public void Binary_BitOr_Range(string expression, int? expectedMin, int? expecte SETestContext.CreateCS(code, "int i, int j").Validator.TagValue("Value").Should().HaveOnlyConstraints(ObjectConstraint.NotNull); } } + + [DataTestMethod] + [DataRow(0b0000, 0b0000, 0b0000)] + [DataRow(0b0101, 0b0101, 0b0000)] + [DataRow(0b0101, 0b0001, 0b0100)] + [DataRow(0b1010, 0b0110, 0b1100)] + [DataRow(0b1010, 0b0000, 0b1010)] + [DataRow(0b1111, 0b1111, 0b0000)] + [DataRow(5, -5, -2)] + [DataRow(5, -4, -7)] + [DataRow(-5, -5, 0)] + [DataRow(-5, -4, 7)] + public void Binary_BitXor_SingleValue(int left, int right, int expected) + { + var code = $""" + var left = {left}; + var right = {right}; + var value = left ^ right; + Tag("Value", value); + """; + SETestContext.CreateCS(code).Validator.TagValue("Value").Should().HaveOnlyConstraints(ObjectConstraint.NotNull, NumberConstraint.From(expected)); + } + + [DataTestMethod] + [DataRow("i >= 4 && j >= 6", 0, null)] + [DataRow("i >= 4 && j >= -6", null, null)] + [DataRow("i >= 4 && j <= 6", null, null)] + [DataRow("i >= 4 && j <= -6", null, -1)] + [DataRow("i >= -4 && j >= 6", null, null)] + [DataRow("i >= -4 && j >= -6", null, null)] + [DataRow("i >= -4 && j <= 6", null, null)] + [DataRow("i >= -4 && j <= -6", null, null)] // exact range: null, -1 + [DataRow("i == 4 && j >= 6", 2, null)] + [DataRow("i == 4 && j >= -6", -8, null)] + [DataRow("i == 4 && j <= 6", null, 7)] + [DataRow("i == 4 && j <= -6", null, -1)] // exact range: null, -2 + [DataRow("i == -4 && j >= 6", null, -1)] // exact range: null, -5 + [DataRow("i == -4 && j >= -6", null, 7)] + [DataRow("i == -4 && j <= 6", -8, null)] + [DataRow("i == -4 && j <= -6", 2, null)] // exact range: 4, null + [DataRow("i <= 4 && j >= 6", null, null)] + [DataRow("i <= 4 && j >= -6", null, null)] + [DataRow("i <= 4 && j <= 6", null, null)] + [DataRow("i <= 4 && j <= -6", null, null)] + [DataRow("i <= -4 && j >= 6", null, -1)] + [DataRow("i <= -4 && j >= -6", null, null)] + [DataRow("i <= -4 && j <= 6", null, null)] + [DataRow("i <= -4 && j <= -6", 0, null)] + [DataRow("i >= 4 && j >= 6 && j <= 8", 0, null)] + [DataRow("i >= 4 && j >= 1 && j <= 3", 1, null)] // exact range: 4, null + [DataRow("i >= 4 && i <= 6 && j >= 6", 0, null)] + [DataRow("i >= 4 && i <= 6 && j >= 6 && j <= 8", 0, 15)] // exact range: 0, 14 + [DataRow("i >= 4 && i <= 5 && j >= 6 && j <= 8", 1, 15)] // exact range: 2, 13 + [DataRow("i >= -3 && i <= -1 && j >= -4 && j <=-2", 0, 7)] // exact range: 0, 3 + [DataRow("i >= -4 && i <= -3 && j >= -2 && j <=-1", 1, 7)] // exact range: 2, 3 + [DataRow("i >= -4 && i <= 6 && j >= -3 && j <= 8", -16, 15)] // exact range: -12, 14 + public void Binary_BitXor_Range(string expression, int? expectedMin, int? expectedMax) + { + var code = $$""" + if ({{expression}}) + { + var value = i ^ j; + Tag("Value", value); + } + """; + + if (expectedMin is not null || expectedMax is not null) + { + SETestContext.CreateCS(code, "int i, int j").Validator.TagValue("Value").Should().HaveOnlyConstraints(ObjectConstraint.NotNull, NumberConstraint.From(expectedMin, expectedMax)); + } + else + { + SETestContext.CreateCS(code, "int i, int j").Validator.TagValue("Value").Should().HaveOnlyConstraints(ObjectConstraint.NotNull); + } + } } diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/SymbolicExecution/Roslyn/CalculationsShouldNotOverflow.cs b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/SymbolicExecution/Roslyn/CalculationsShouldNotOverflow.cs index c2970daa71a..27565f6d2d6 100644 --- a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/SymbolicExecution/Roslyn/CalculationsShouldNotOverflow.cs +++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/SymbolicExecution/Roslyn/CalculationsShouldNotOverflow.cs @@ -115,7 +115,7 @@ public void BasicOperators() _ = i * 2147483600; // Noncompliant i = 2 ^ j; - _ = i * 2147483600; // FN + _ = i * 2147483600; // Noncompliant i = 2 % j; _ = i * 2147483600; // FN diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/SymbolicExecution/Roslyn/CalculationsShouldNotOverflow.vb b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/SymbolicExecution/Roslyn/CalculationsShouldNotOverflow.vb index 9d08f8490e1..59e080a549a 100644 --- a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/SymbolicExecution/Roslyn/CalculationsShouldNotOverflow.vb +++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/SymbolicExecution/Roslyn/CalculationsShouldNotOverflow.vb @@ -76,7 +76,7 @@ Public Class Sample __ = i * 2147483600 ' Noncompliant i = 2 Xor j - __ = i * 2147483600 ' FN + __ = i * 2147483600 ' Noncompliant i = 2 Mod j __ = i * 2147483600 ' FN