diff --git a/analyzers/src/SonarAnalyzer.Common/SymbolicExecution/Roslyn/OperationProcessors/Binary.cs b/analyzers/src/SonarAnalyzer.Common/SymbolicExecution/Roslyn/OperationProcessors/Binary.cs index c553fae5cb0..874a1824713 100644 --- a/analyzers/src/SonarAnalyzer.Common/SymbolicExecution/Roslyn/OperationProcessors/Binary.cs +++ b/analyzers/src/SonarAnalyzer.Common/SymbolicExecution/Roslyn/OperationProcessors/Binary.cs @@ -43,11 +43,13 @@ protected override ProgramState LearnBranchingConstraint(ProgramState state, IBi private static ProgramState LearnBranchingConstraint(ProgramState state, IBinaryOperationWrapper binary, bool falseBranch) where T : SymbolicConstraint { + var useOpposite = falseBranch ^ binary.OperatorKind.IsNotEquals(); // We can fall through ?? because "constraint" and "testedSymbol" are exclusive. Symbols with the constraint will be recognized as "constraint" side. if ((OperandConstraint(binary.LeftOperand) ?? OperandConstraint(binary.RightOperand)) is { } constraint - && (OperandSymbolWithoutConstraint(binary.LeftOperand) ?? OperandSymbolWithoutConstraint(binary.RightOperand)) is { } testedSymbol) + && (OperandSymbolWithoutConstraint(binary.LeftOperand) ?? OperandSymbolWithoutConstraint(binary.RightOperand)) is { } testedSymbol + && !(useOpposite && constraint is BoolConstraint && testedSymbol.GetSymbolType().IsNullableBoolean())) // We don't want to learn False for "nullableBool != true", because it could also be . { - constraint = constraint.ApplyOpposite(falseBranch ^ binary.OperatorKind.IsNotEquals()); + constraint = constraint.ApplyOpposite(useOpposite); // Beware that opposite of ObjectConstraint.NotNull doesn't exist and returns return constraint is null ? null : state.SetSymbolConstraint(testedSymbol, constraint); } else diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/SymbolicExecution/Roslyn/RoslynSymbolicExecutionTest.Branching.LearnConstraint.cs b/analyzers/tests/SonarAnalyzer.UnitTest/SymbolicExecution/Roslyn/RoslynSymbolicExecutionTest.Branching.LearnConstraint.cs index 5c0c9066cc4..22a451ce551 100644 --- a/analyzers/tests/SonarAnalyzer.UnitTest/SymbolicExecution/Roslyn/RoslynSymbolicExecutionTest.Branching.LearnConstraint.cs +++ b/analyzers/tests/SonarAnalyzer.UnitTest/SymbolicExecution/Roslyn/RoslynSymbolicExecutionTest.Branching.LearnConstraint.cs @@ -177,7 +177,7 @@ public void Branching_LearnsObjectConstraint_CS(string expression) [DataRow("!(bool)(object)!!(null != arg)")] [DataRow("!!!((object)arg != (object)null)")] [DataRow("!!!((object)null != (object)arg)")] - public void Branching_LearnsObjectConstraint_Nullable_CS(string expression) + public void Branching_LearnsObjectConstraint_NullableInt_CS(string expression) { var validator = CreateIfElseEndValidatorCS(expression, OperationKind.Binary, "int?"); validator.ValidateTag("If", x => x.HasConstraint(ObjectConstraint.Null).Should().BeTrue()); @@ -187,6 +187,27 @@ public void Branching_LearnsObjectConstraint_Nullable_CS(string expression) .And.ContainSingle(x => x.HasConstraint(ObjectConstraint.NotNull)); } + [DataTestMethod] + [DataRow("arg == null")] + [DataRow("null == arg")] + [DataRow("(object)(object)arg == (object)(object)null")] + [DataRow("(object)(object)arg == (object)(object)isNull")] + [DataRow("!!!(arg != null)")] + [DataRow("!!!(null != arg)")] + [DataRow("!(bool)(object)!!(arg != null)")] + [DataRow("!(bool)(object)!!(null != arg)")] + [DataRow("!!!((object)arg != (object)null)")] + [DataRow("!!!((object)null != (object)arg)")] + public void Branching_LearnsObjectConstraint_NullableBool_CS(string expression) + { + var validator = CreateIfElseEndValidatorCS(expression, OperationKind.Binary, "bool?"); + validator.ValidateTag("If", x => x.HasConstraint(ObjectConstraint.Null).Should().BeTrue()); + validator.ValidateTag("Else", x => x.HasConstraint(ObjectConstraint.NotNull).Should().BeTrue()); + validator.TagValues("End").Should().HaveCount(2) + .And.ContainSingle(x => x.HasConstraint(ObjectConstraint.Null)) + .And.ContainSingle(x => x.HasConstraint(ObjectConstraint.NotNull)); + } + [DataTestMethod] [DataRow("arg != null")] [DataRow("arg != isNull")] @@ -217,7 +238,7 @@ public void Branching_LearnsObjectConstraint_Binary_Negated_CS(string expression [DataRow("!!!(null == arg)")] [DataRow("!(bool)(object)!!(arg == null)")] [DataRow("!(bool)(object)!!(null == arg)")] - public void Branching_LearnsObjectConstraint_Binary_Negated_Nullable_CS(string expression) + public void Branching_LearnsObjectConstraint_Binary_Negated_NullableInt_CS(string expression) { var validator = CreateIfElseEndValidatorCS(expression, OperationKind.Binary, "int?"); validator.ValidateTag("If", x => x.HasConstraint(ObjectConstraint.NotNull).Should().BeTrue()); @@ -228,11 +249,36 @@ public void Branching_LearnsObjectConstraint_Binary_Negated_Nullable_CS(string e } [DataTestMethod] - [DataRow("arg == isObject")] - [DataRow("isObject == arg")] - public void Branching_LearnsObjectConstraint_Binary_UndefinedInOtherBranch_CS(string expression) + [DataRow("arg != null")] + [DataRow("null != arg")] + [DataRow("(object)(object)arg != (object)(object)null")] + [DataRow("(object)(object)arg != (object)(object)isNull")] + [DataRow("!!!(arg == null)")] + [DataRow("!!!(null == arg)")] + [DataRow("!(bool)(object)!!(arg == null)")] + [DataRow("!(bool)(object)!!(null == arg)")] + public void Branching_LearnsObjectConstraint_Binary_Negated_NullableBool_CS(string expression) { - var validator = CreateIfElseEndValidatorCS(expression, OperationKind.Binary); + var validator = CreateIfElseEndValidatorCS(expression, OperationKind.Binary, "bool?"); + validator.ValidateTag("If", x => x.HasConstraint(ObjectConstraint.NotNull).Should().BeTrue()); + validator.ValidateTag("Else", x => x.HasConstraint(ObjectConstraint.Null).Should().BeTrue()); + validator.TagValues("End").Should().HaveCount(2) + .And.ContainSingle(x => x.HasConstraint(ObjectConstraint.Null)) + .And.ContainSingle(x => x.HasConstraint(ObjectConstraint.NotNull)); + } + + [DataTestMethod] + [DataRow("arg == isObject", "object")] + [DataRow("isObject == arg", "object")] + [DataRow("arg == true", "bool?")] + [DataRow("arg == false", "bool?")] + [DataRow("true == arg", "bool?")] + [DataRow("false == arg", "bool?")] + [DataRow("arg == 42", "int?")] + [DataRow("42 == arg", "int?")] + public void Branching_LearnsObjectConstraint_Binary_UndefinedInOtherBranch_CS(string expression, string argType) + { + var validator = CreateIfElseEndValidatorCS(expression, OperationKind.Binary, argType); validator.ValidateTag("If", x => x.HasConstraint(ObjectConstraint.NotNull).Should().BeTrue()); validator.ValidateTag("Else", x => x.Should().BeNull("We can't tell if it is Null or NotNull in this branch")); validator.TagValues("End").Should().HaveCount(2) @@ -241,11 +287,17 @@ public void Branching_LearnsObjectConstraint_Binary_UndefinedInOtherBranch_CS(st } [DataTestMethod] - [DataRow("arg != isObject")] - [DataRow("isObject != arg")] - public void Branching_LearnsObjectConstraint_Binary_UndefinedInOtherBranch_Negated_CS(string expression) + [DataRow("arg != isObject", "object")] + [DataRow("isObject != arg", "object")] + [DataRow("arg != true", "bool?")] + [DataRow("arg != false", "bool?")] + [DataRow("true != arg", "bool?")] + [DataRow("false != arg", "bool?")] + [DataRow("arg != 42", "int?")] + [DataRow("42 != arg", "int?")] + public void Branching_LearnsObjectConstraint_Binary_UndefinedInOtherBranch_Negated_CS(string expression, string argType) { - var validator = CreateIfElseEndValidatorCS(expression, OperationKind.Binary); + var validator = CreateIfElseEndValidatorCS(expression, OperationKind.Binary, argType); validator.ValidateTag("If", x => x.Should().BeNull("We can't tell if it is Null or NotNull in this branch")); validator.ValidateTag("Else", x => x.HasConstraint(ObjectConstraint.NotNull).Should().BeTrue()); validator.TagValues("End").Should().HaveCount(2) @@ -393,8 +445,11 @@ public void Branching_LearnsObjectConstraint_ConstantPattern_Null_Negated(string [DataTestMethod] [DataRow("arg is true")] [DataRow("arg is true", "bool")] + [DataRow("arg is true", "bool?")] [DataRow("!!(arg is true)")] + [DataRow("!!(arg is true)", "bool?")] [DataRow("arg is not not true")] + [DataRow("arg is not not true", "bool?")] public void Branching_LearnsObjectConstraint_ConstantPattern_True(string expression, string argType = "object") { var validator = CreateIfElseEndValidatorCS(expression, OperationKind.ConstantPattern, argType); @@ -408,6 +463,7 @@ public void Branching_LearnsObjectConstraint_ConstantPattern_True(string express [DataTestMethod] [DataRow("arg is not true", "object")] [DataRow("arg is not true", "bool")] + [DataRow("arg is not true", "bool?")] public void Branching_LearnsObjectConstraint_ConstantPattern_True_Negated(string expression, string argType) { var validator = CreateIfElseEndValidatorCS(expression, OperationKind.ConstantPattern, argType); @@ -433,6 +489,7 @@ public void Branching_LearnsObjectConstraint_ConstantPattern_False(string expres [DataTestMethod] [DataRow("arg is 42", "int")] + [DataRow("arg is 42", "int?")] [DataRow("arg is 42", "T")] [DataRow("arg is 42", "TStruct")] public void Branching_LearnsObjectConstraint_ConstantPattern_ValueTypes_InputIsNotReferenceType(string expression, string argType) @@ -447,6 +504,7 @@ public void Branching_LearnsObjectConstraint_ConstantPattern_ValueTypes_InputIsN [DataRow(@"arg is ""some text""")] [DataRow(@"arg is """"")] [DataRow("arg is 42")] + [DataRow("arg is 42", "int?")] [DataRow("arg is System.ConsoleKey.Enter")] // Enum [DataRow("arg is 42", "TClass")] [DataRow("arg is 42", "IComparable")] // arg is either a class implementing the interface or a boxed value type