diff --git a/analyzers/src/SonarAnalyzer.Common/SymbolicExecution/Roslyn/OperationProcessors/References.cs b/analyzers/src/SonarAnalyzer.Common/SymbolicExecution/Roslyn/OperationProcessors/References.cs index cc2c9fe04ae..a19ab926ab6 100644 --- a/analyzers/src/SonarAnalyzer.Common/SymbolicExecution/Roslyn/OperationProcessors/References.cs +++ b/analyzers/src/SonarAnalyzer.Common/SymbolicExecution/Roslyn/OperationProcessors/References.cs @@ -73,16 +73,28 @@ protected override IPropertyReferenceOperationWrapper Convert(IOperation operati protected override ProgramState Process(SymbolicContext context, IPropertyReferenceOperationWrapper propertyReference) { + var state = context.State; if (propertyReference.Instance.TrackedSymbol() is { } symbol) { - var state = context.State.SetSymbolConstraint(symbol, ObjectConstraint.NotNull); - return propertyReference.Property.Name == "Value" && propertyReference.Instance.Type.IsNullableValueType() && context.State[symbol] is { } value - ? state.SetOperationValue(context.Operation, value) - : state; + if (propertyReference.Instance.Type.IsNullableValueType()) + { + if (propertyReference.Property.Name == "Value" && state[symbol] is { } value) + { + state = state.SetOperationValue(context.Operation, value); + } + else if (propertyReference.Property.Name == "HasValue") + { + // Return directly, do not set NotNull on the symbol itself + return state[symbol]?.Constraint() is { } objectConstraint + ? state.SetOperationConstraint(context.Operation, BoolConstraint.From(objectConstraint == ObjectConstraint.NotNull)) + : state; + } + } + return state.SetSymbolConstraint(symbol, ObjectConstraint.NotNull); } else { - return context.State; + return state; } } } diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/SymbolicExecution/Roslyn/RoslynSymbolicExecutionTest.Nullable.cs b/analyzers/tests/SonarAnalyzer.UnitTest/SymbolicExecution/Roslyn/RoslynSymbolicExecutionTest.Nullable.cs index ba8e0050877..b0c2bbc06d3 100644 --- a/analyzers/tests/SonarAnalyzer.UnitTest/SymbolicExecution/Roslyn/RoslynSymbolicExecutionTest.Nullable.cs +++ b/analyzers/tests/SonarAnalyzer.UnitTest/SymbolicExecution/Roslyn/RoslynSymbolicExecutionTest.Nullable.cs @@ -59,4 +59,30 @@ public void Nullable_Value_ReadsConstraintsFromInstance() validator.ValidateTag("FalseFirst", x => x.HasConstraint(BoolConstraint.False).Should().BeTrue()); validator.ValidateTag("FalseFirst", x => x.HasConstraint(TestConstraint.First).Should().BeTrue()); } + + [TestMethod] + public void Nullable_HasValue_ReadsBoolConstraintFromObjectConstraint() + { + const string code = """ + var hasValue = arg.HasValue; + Tag("HasValueUnknown", hasValue); + Tag("SymbolUnknown", arg); + arg = true; + hasValue = arg.HasValue; + Tag("HasValueAfterTrue", hasValue); + Tag("SymbolAfterTrue", arg); + arg = null; + hasValue = arg.HasValue; + Tag("HasValueAfterNull", hasValue); + Tag("SymbolAfterNull", arg); + """; + var validator = SETestContext.CreateCS(code, ", bool? arg").Validator; + validator.ValidateTag("HasValueUnknown", x => x.Should().BeNull()); + validator.ValidateTag("SymbolUnknown", x => x.Should().BeNull()); + validator.ValidateTag("HasValueAfterTrue", x => x.Should().BeNull()); // ToDo: Should be x.HasConstraint(BoolConstraint.True).Should().BeTrue()); after we build NotNull for bool literal. + validator.ValidateTag("SymbolAfterTrue", x => x.HasConstraint(BoolConstraint.True).Should().BeTrue()); + validator.ValidateTag("SymbolAfterTrue", x => x.HasConstraint(ObjectConstraint.NotNull).Should().BeFalse()); // ToDo: Should be BeTrue() after we build NotNull for bool literal + validator.ValidateTag("HasValueAfterNull", x => x.HasConstraint(BoolConstraint.False).Should().BeTrue()); + validator.ValidateTag("SymbolAfterNull", x => x.HasConstraint(ObjectConstraint.Null).Should().BeTrue()); + } }