Support negative null-checks when we offer a fix to use pattern-matching#20246
Support negative null-checks when we offer a fix to use pattern-matching#20246sharwell merged 1 commit intodotnet:dev15.7.xfrom
Conversation
|
Thanks and sorry! Tagging the rest of @dotnet/roslyn-ide for review too. |
|
Please hold off the review untill the linked PR goes in. Thanks. |
71c0bcf to
d56b078
Compare
|
@sharwell this is green, ready for review. thanks. |
|
@sharwell any chance we can have this for 15.6? |
There was a problem hiding this comment.
❗️ (Unless not possible) it would be good to retain the original if statement as well as the new one.
There was a problem hiding this comment.
I just changed this test to cover negative case.
There was a problem hiding this comment.
❓ Is the ability to handle a cast to object new for both the normal and negated forms? If so, we should add a test for the newly supported normal form.
There was a problem hiding this comment.
Yes, it's new, I'll add the test for (object)x != null as well.
There was a problem hiding this comment.
💭 This will rule out the feature for many types. We may be able to improve the experience by allowing user-defined operators, but ensuring a single Fix All operation only corrects operations that use the same operator. For example:
- If all uses of
== nulland!= nullfor a variable use the default operator, a Fix All on the variable will fix all cases where no user-defined operators are involved - If one or more uses of
==or!=involve a user-defined operator for a variable with the declared typeT, then a Fix All on that variable will fix all instances involving variables with declared typeT, whether or not they use user-defined operators. Alternately, this scenario could fix all instances involving variables with declared typeTwhich use one or more user-defined operators.
The equivalence ID of the code fix can be used to "bucket" fixes according to the above conditions.
If you like the idea but aren't ready to implement it, feel free to leave this as a follow-up issue by filing a new bug referencing this comment.
There was a problem hiding this comment.
This is not part of this PR, the only diff here is notEquals -> comparison
There was a problem hiding this comment.
❓ To help me get my head around this, can you provide an example of why this block is necessary?
There was a problem hiding this comment.
if (!(x is Foo foo)) {}
// foo is always definitely assigned (we don't need to check so we return early)
while (!(x is Foo foo)) {}
// foo is not in scope (so we need to check if it's accessed)There was a problem hiding this comment.
can you put this in as a comment.
There was a problem hiding this comment.
Wait, this is only correct if if has an unreachable endpoint. I'll remove this check.
There was a problem hiding this comment.
💭 This seems like an unnecessary change
There was a problem hiding this comment.
Relates to #21962
I can revert it if you want.
There was a problem hiding this comment.
is it worth to use Task.WhenAll here?
There was a problem hiding this comment.
This would be a candidate for using Theory instead of Fact. With that said, Theory is used sparingly (very rarely currently) and not everyone has agreed that it's a good idea. I'm only showing the difference here as a comparison. The one advantage I've found is a test failure shows more information about the specific failing case, as opposed to just a line number in a method with many tests.
[Theory, Trait(Traits.Feature, Traits.Features.CodeActionsInlineTypeCheck)]
[InlineData("x != null", "o is string x")]
[InlineData("null != x", "o is string x")]
[InlineData("(object)x != null", "o is string x")]
[InlineData("null != (object)x", "o is string x")]
// Other test cases added as additional attributes...
public async Task InlineTypeCheck1(string input, string output)
{
await TestStatement($"if ({input}) {{ }}", $"if ({output}) {{ }}");
await TestStatement($"var y = {input};", $"var y = {output};");
await TestStatement($"return {input};", $"return {output};");
}
sharwell
left a comment
There was a problem hiding this comment.
I was hesitant to start with due to test churn, but now that I've been through it in more detail I think this is a great change.
There was a problem hiding this comment.
This would be a candidate for using Theory instead of Fact. With that said, Theory is used sparingly (very rarely currently) and not everyone has agreed that it's a good idea. I'm only showing the difference here as a comparison. The one advantage I've found is a test failure shows more information about the specific failing case, as opposed to just a line number in a method with many tests.
[Theory, Trait(Traits.Feature, Traits.Features.CodeActionsInlineTypeCheck)]
[InlineData("x != null", "o is string x")]
[InlineData("null != x", "o is string x")]
[InlineData("(object)x != null", "o is string x")]
[InlineData("null != (object)x", "o is string x")]
// Other test cases added as additional attributes...
public async Task InlineTypeCheck1(string input, string output)
{
await TestStatement($"if ({input}) {{ }}", $"if ({output}) {{ }}");
await TestStatement($"var y = {input};", $"var y = {output};");
await TestStatement($"return {input};", $"return {output};");
}There was a problem hiding this comment.
📝 This test case covers the original InlineTypeCheck1.
There was a problem hiding this comment.
📝 This test case covers the original InlineTypeCheckInvertedCheck1.
There was a problem hiding this comment.
📝 This test case replaces the original TestMissingOnNullEquality, showing new functionality.
There was a problem hiding this comment.
📝 This test case covers the original TestInlineNullCheck1.
There was a problem hiding this comment.
❗️ Use an explicitly-typed variable rather than a cast. This ensures a compilation error occurs for any case where an implicit cast is not available (which in theory ensures the operation is a widening conversion, though we have cases even in this codebase where that is not the case).
There was a problem hiding this comment.
I use Kind() where the code asserts that the variable is never null i.e the path is unreachable if it was. This is also the convention used on the compiler side.
There was a problem hiding this comment.
I suppose on the IDE side we traditionally use IsKind to not throw if anything ever failed.
There was a problem hiding this comment.
💡 definitely
💡 I find this block pleasantly readable, but it may be because I have context from seeing the recent bug. Consider expanding this comment by adding the following:
// For example:
//
// if ((x = o as string) != null || SomeExpression) {
// // x would not be definitely assigned here if pattern matching were used
// }There was a problem hiding this comment.
💡 A comment regarding the return value would be handy
There was a problem hiding this comment.
💡 Assuming my understanding is correct, the following might be good to add.
// For example:
//
// if (!(o is string x)) {
// return;
// }
//
// // The 'return' statement above ensures x is definitely assigned here
// Console.WriteLine(x);|
Requesting a review from @agocke to verify the use of data flow analysis.
|
I'm usually one how pushes back on Theories. That said, if things stay very simple, and it's clear precisely what the theory is testing/expecting, then i'm totally ok with it. Where i usually don't like them is where people parameterize 6 places in a test and it becomes totally unclear waht's actually happening :) In this case, theories look fine to me. If we use them judiciously and keep clarity high, then i'm all for them :) |
There was a problem hiding this comment.
There is a bug here, should be missing but it's not:
void M()
{
[|var|] x = o as string;
if (x != null)
{
}
Console.WriteLine(x);
x = "";
}x is always assigned but there is a read before assignment - probably need to revert code to iterate over statements one by one, @CyrusNajmabadi, can you confirm if that was the original purpose of doing so instead of region analysis?
There was a problem hiding this comment.
There is a bug here, should be missing but it's not:
It's missing for me. I'm not able to see this locally.
There was a problem hiding this comment.
Note: iterating over statements is not safe because assignments could be anywhere in an expression,
Console.WriteLine(x = Read(x));There was a problem hiding this comment.
@CyrusNajmabadi I meant in this branch. The previous revision check for nodes statement by statement so doesn't have this bug.
However, with either approaches, this one is not missing.
string Use(string x) => x;
void M()
{
var x = o as string;
if (x != null)
{
}
Console.WriteLine(x = Use(x));
}From data flow analysis results, we can't see if the read is happened before write..
There was a problem hiding this comment.
@CyrusNajmabadi, can you confirm if that was the original purpose of doing so instead of region analysis?
Yes, i believe that was the intent.
Note: iterating over statements is not safe because assignments could be anywhere in an expression,
I thought we checked assignment on a per-reference basis. So we would specifically check the Use(x) and see the problem. Right?
There was a problem hiding this comment.
I think we return early in the previous code. we see the write first and bail. I'll try a mixed approach, if we are AlwaysAssgined, check if there is a read, if yes, need to walk through tree to see it doesn't come first. early return here could be safe.
There was a problem hiding this comment.
Oh, i think i see what you're saying. The logic here is buggy:
Specifically, because it sees the x =, it assumes that 'x' is always assigned, and thus the change is safe. But it's not. Because the right side of the assignment executes first, and will do a read of the unassigned variable.
- That sucks. But it's also a very corner case.
- We could consider fixing it. Namely that if the reference is the left of an assign, that we have to ensure that the right side is fine as well. Annoying, but doable.
There was a problem hiding this comment.
Note: using AlwaysAssigned is necessary to handle branches e.g. #21097
There was a problem hiding this comment.
I see. The logic here has changed a bit, and i'll admit i don't really fully understand the algorithm as it stands now, so i'm not sure the exact right fix. But i do think this can be solved.
Also, an efficiency point:
var symbol = semanticModel.GetSymbolInfo(id, cancellationToken).GetAnySymbol();
if (localVariable.Equals(symbol))
{We should check if the id.ValueText matches the local variable name before bothering to do the binding. That will avoid a lot of expensive semantic checks.
BTW, something to consider: Right now the code is very concept/information dense. I.e. it's trying to figure out all the cases and encode it all into these variables that it's working with. You should always feel free to consider decomposing this into the major cases that need to be answered. i.e.: if (ThereIsReadBeforeAssignmentInFollowingStatements(...) ||
ThereIsSomethingWithTheElseClause(...) ||
ThereIsSomeOtherCaseThatsAlsoImportant(...))
{
return;
}Then make each of those approaches clear as to what tehy're checking for and how they do it. I'm not saying this will improve things. But it's something to consider when code starts getting very dense and difficult to understand because it's trying to handle so many cases. |
|
BTW, is there a reason we don't just do:
I thought we already did this approach for some fix providers. It means not actually having to try to reimplement all the compiler's flow control logic into the IDE. |
|
We already have CSharpInlineDeclarationDiagnosticAnalyzer, which has: This is essentially the exact same problem. We're "inlining" the variable declaration into the pattern. We should just extract out a helper here that can be used for both analyzers. Now, this check is expensive. So ideally we'd just do some quicker checks first (like what existed in V1 of this analyzer). But if those quick checks don't see a problem, we do the expensive work. |
|
The diff doesn't help much as I rewrote the def assignment check part. Please review the final file. Thanks. |
|
tagging @CyrusNajmabadi |
There was a problem hiding this comment.
it woudl be helpful to have examples shown in the code of what this is catching.
There was a problem hiding this comment.
it woudl be helpful to have examples shown in the code of what this is catching.
|
@dotnet-bot test windows_release_vs-integration_prtest please |
|
tagging @sharwell, this is ready to go in with 2 approved reviews. |
|
Approved for 15.7 - sorry for the delay. Note that it will need to be re-targeted to the |
|
@alrz This needs to be rebased on the dev15.7.x branch. Please let me know if you would like me to do the rebase or if you would like to do it yourself. 😄 |
|
will rebase when I get home |
|
Had to squash commits, it got muddled up in the middle of rebase |
Follow up: #20240