Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SE: Learn bool constraint from Equals when parameters are numerical values #7906

Merged
merged 6 commits into from
Aug 31, 2023

Conversation

mary-georgiou-sonarsource
Copy link
Contributor

Fixes #7704

@@ -186,15 +186,30 @@ private static ProgramState ProcessAssertedBoolSymbol(ProgramState state, IOpera
invocation switch
{
{ Arguments.Length: 2, TargetMethod.IsStatic: true } => ProcessEquals(context, invocation.Arguments[0].ToArgument().Value, invocation.Arguments[1].ToArgument().Value),
{ Arguments.Length: 1 } when invocation.TargetMethod.ContainingType.IsNullableValueType() => ProcessEquals(context, invocation.Instance, invocation.Arguments[0].ToArgument().Value),
{ Arguments.Length: 1 } when invocation.TargetMethod.ContainingType.IsNullableValueType() || invocation.TargetMethod.ContainingType.IsStruct() =>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not super confident about this.
This is so we can process also invocations for 1.Equals(2) and false.Equals(true).
I limited it only to structs as for classes it might be overridden and create weird side efffects.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm surprised this was limited to nullables in the first place. Seems like a reasonable improvement to me.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I can be very specific here again and ask that the containing type is bool or int?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You would need to include all the integral types (long, byte etc.). Keep it as is.

Comment on lines -1048 to -1050
[DataRow("42", "42")]
[DataRow("42", "0")]
[DataRow("0", "42")]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of these are now detected as equal.

Copy link
Contributor

@Tim-Pohlmann Tim-Pohlmann left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good for the most part. Just some improvements.

@@ -186,15 +186,30 @@ private static ProgramState ProcessAssertedBoolSymbol(ProgramState state, IOpera
invocation switch
{
{ Arguments.Length: 2, TargetMethod.IsStatic: true } => ProcessEquals(context, invocation.Arguments[0].ToArgument().Value, invocation.Arguments[1].ToArgument().Value),
{ Arguments.Length: 1 } when invocation.TargetMethod.ContainingType.IsNullableValueType() => ProcessEquals(context, invocation.Instance, invocation.Arguments[0].ToArgument().Value),
{ Arguments.Length: 1 } when invocation.TargetMethod.ContainingType.IsNullableValueType() || invocation.TargetMethod.ContainingType.IsStruct() =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm surprised this was limited to nullables in the first place. Seems like a reasonable improvement to me.

&& context.State.Constraint<BoolConstraint>(rightOperation) is { } leftBool
? context.SetOperationConstraint(BoolConstraint.From(leftBool == rightBool)).ToArray()
: ProcessEqualsObject(context, leftOperation, rightOperation);
private static ProgramState[] ProcessEquals(SymbolicContext context, IOperation leftOperation, IOperation rightOperation)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you extract all three options into methods you can do something like this:

ProcessEqualsBool(context, leftOperation, rightOperation)
?? ProcessEqualsNumber(context, leftOperation, rightOperation)
?? ProcessEqualsObject(context, leftOperation, rightOperation);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reverted - it does not look natural to return null when the return type is an array of ProgramState.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pavel-mikula-sonarsource any better way to split ProcessEquals

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's fine as it is.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does it feel unnatural? This is a pattern used quite commonly in the code-base.
I'm not super opposed to the if-else tree, but I find the alternative more readable.

Comment on lines 201 to 205
else if (context.State.Constraint<NumberConstraint>(leftOperation) is { } leftNumber
&& context.State.Constraint<NumberConstraint>(rightOperation) is { } rightNumber
&& leftNumber.IsSingleValue
&& rightNumber.IsSingleValue)
{
return context.SetOperationConstraint(BoolConstraint.From(leftNumber.Equals(rightNumber))).ToArray();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be really good if we would also learn false for non-SingleValues ranges that do not Overlap. It should be very easy to add here. But feel free to not include it in this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds like a good idea :D
I'll see how "easy" it is.

EDIT: Done

Comment on lines 1265 to 1271
var code = $"""
var result = {left}.Equals({right});
Tag("Result", result);

var resultStaticCall = object.Equals({left}, {right});
Tag("ResultStaticCall", resultStaticCall);
""";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
var code = $"""
var result = {left}.Equals({right});
Tag("Result", result);
var resultStaticCall = object.Equals({left}, {right});
Tag("ResultStaticCall", resultStaticCall);
""";
var code = $"""
var result = {left}.Equals({right});
Tag("Result", result);
var resultStaticCall = object.Equals({left}, {right});
Tag("ResultStaticCall", resultStaticCall);
""";

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is correct - there's no need to move it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no reason for the extra indention and I cannot find anything about string literals in our code conventions. Why do you think this is correct?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just followed the same pattern of the other examples in the file - which I now noticed that it's not consistent. :/
I'll update everything to 3 * 4 spaces.

Comment on lines 1280 to 1286
var code = """
if (i>1 && i<10)
{
var result = object.Equals(i, 0);
Tag("Result", result);
}
""";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
var code = """
if (i>1 && i<10)
{
var result = object.Equals(i, 0);
Tag("Result", result);
}
""";
var code = """
if (i>1 && i<10)
{
var result = object.Equals(i, 0);
Tag("Result", result);
}
""";

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

public void Invocation_NumberEquals_DoesNotLearn()
{
var code = """
if (i>1 && i<10)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (i>1 && i<10)
if (i > 1 && i < 10)

}
""";
var validator = SETestContext.CreateCS(code, "int i").Validator;
validator.TagValue("Result").Should().HaveOnlyConstraints(ObjectConstraint.NotNull);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a comment to document the not-implemented behavior.

Suggested change
validator.TagValue("Result").Should().HaveOnlyConstraints(ObjectConstraint.NotNull);
validator.TagValue("Result").Should().HaveOnlyConstraints(ObjectConstraint.NotNull); // Should learn false

Copy link
Contributor

@pavel-mikula-sonarsource pavel-mikula-sonarsource left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with some nitpicks

}
""";
var validator = SETestContext.CreateCS(code, "int i").Validator;
validator.TagValue("Result").Should().HaveOnlyConstraints(ObjectConstraint.NotNull); // Should learn 'false'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is in range, so it could be true or false

Suggested change
validator.TagValue("Result").Should().HaveOnlyConstraints(ObjectConstraint.NotNull); // Should learn 'false'
validator.TagValue("Result").Should().HaveOnlyConstraints(ObjectConstraint.NotNull);

public void Invocation_NumberEquals_RangesDoNotOverlap_LearnsResult()
{
var code = $$"""
if (i > 0 && j < 0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same thing, easier to imagine (negative first, on the left of the axis)

Suggested change
if (i > 0 && j < 0)
if (i < 0 && j > 0)

}

[TestMethod]
public void Invocation_NumberEquals_DoesNotLearn()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs to explain how is it different from the others

Suggested change
public void Invocation_NumberEquals_DoesNotLearn()
public void Invocation_NumberEquals_ValueInRange_DoesNotLearn()

Comment on lines 196 to 197
if (context.State.Constraint<BoolConstraint>(leftOperation) is { } rightBool
&& context.State.Constraint<BoolConstraint>(rightOperation) is { } leftBool)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Swapped names

Suggested change
if (context.State.Constraint<BoolConstraint>(leftOperation) is { } rightBool
&& context.State.Constraint<BoolConstraint>(rightOperation) is { } leftBool)
if (context.State.Constraint<BoolConstraint>(leftOperation) is { } leftBool
&& context.State.Constraint<BoolConstraint>(rightOperation) is { } rightBool)

Comment on lines +217 to +224
else if (!left.Overlaps(right))
{
return context.SetOperationConstraint(BoolConstraint.False);
}
else
{
return context.State;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would make the condition positive

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I make it positive then I don't know the outcome.
If the number constrains don't overlap I know that the condition is false - if they do overlap then I know nothing.

Copy link
Contributor

@Tim-Pohlmann Tim-Pohlmann left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll approve this besides some open discussions, since I think they should not be blocking.
Proceed on based on your own judgment.

&& context.State.Constraint<BoolConstraint>(rightOperation) is { } leftBool
? context.SetOperationConstraint(BoolConstraint.From(leftBool == rightBool)).ToArray()
: ProcessEqualsObject(context, leftOperation, rightOperation);
private static ProgramState[] ProcessEquals(SymbolicContext context, IOperation leftOperation, IOperation rightOperation)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does it feel unnatural? This is a pattern used quite commonly in the code-base.
I'm not super opposed to the if-else tree, but I find the alternative more readable.

Comment on lines 1265 to 1271
var code = $"""
var result = {left}.Equals({right});
Tag("Result", result);

var resultStaticCall = object.Equals({left}, {right});
Tag("ResultStaticCall", resultStaticCall);
""";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no reason for the extra indention and I cannot find anything about string literals in our code conventions. Why do you think this is correct?

@sonarcloud
Copy link

sonarcloud bot commented Aug 31, 2023

Kudos, SonarCloud Quality Gate passed!    Quality Gate passed

Bug A 0 Bugs
Vulnerability A 0 Vulnerabilities
Security Hotspot A 0 Security Hotspots
Code Smell A 0 Code Smells

No Coverage information No Coverage information
No Duplication information No Duplication information

@sonarcloud
Copy link

sonarcloud bot commented Aug 31, 2023

Kudos, SonarCloud Quality Gate passed!    Quality Gate passed

Bug A 0 Bugs
Vulnerability A 0 Vulnerabilities
Security Hotspot A 0 Security Hotspots
Code Smell A 0 Code Smells

100.0% 100.0% Coverage
0.0% 0.0% Duplication

@mary-georgiou-sonarsource mary-georgiou-sonarsource merged commit 9e6cc0d into master Aug 31, 2023
22 checks passed
@mary-georgiou-sonarsource mary-georgiou-sonarsource deleted the mary/equals-numerical branch August 31, 2023 12:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

SE: Fix FN S2583/S2589 Equals
3 participants