-
Notifications
You must be signed in to change notification settings - Fork 4.1k
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
Fix nested nullability of collection expressions #74490
Conversation
// TODO2: what does it mean for these types to differ? Why is it correct to copy over the top-level nullability when the types have differences beyond nullability? | ||
if (!visitResult.LValueType.Type.Equals(conversionOpt.Type, TypeCompareKind.AllNullableIgnoreOptions)) | ||
{ | ||
visitResult = withType(visitResult, conversionOpt.Type); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wasn't able to figure out exactly what to do here. I found that sometimes the visitResult had a nullable-enriched type prior to this call and that calling withType
was stomping on that and using the type from initial binding. That was causing the enriched type info from this change to get dropped.
But, other times, the type of conversionOpt.Type
was different than visitResult
beyond just nullability. In this case, I opted to just drop the type from the visitResult as the original code was doing. It seems most likely that there is no "nullable-enriched" version of the type resulting from the visit, which directly corresponds to the conversionOpt.Type
. #Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From offline discussion, TrackAnalyzedNullabilityThroughConversionGroup
is incorrect if we think of the general nullability tracking through conversions. But that's not its job (VisitConversion
does that full analysis). So only a subset of cases must come through here (error conversions? trivial conversions?). An assertion narrowing down the conversions we expect to see here would help.
Is it okay that it returns types with nullabilities from initial binding in some/many cases?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've investigated this more.
I noticed that SetAnalyzedNullability is already dropping the visitResult's type if it differs from conversionOpt.Type (with all ignore options). So, I think that 'withType' never actually affects anything. It was essentially repeating a step that SetAnalyzedNullability was already doing.
I have removed the function, and added a comment about the "inaccuracy" of what we are doing here--which is just that we're using the conversion's type from initial binding, instead of using the operand's visit result to reinfer the conversion's type and store the reinferred type instead.
My change has made it so that when the conversion operand's nested nullability is meaningfully reinferred, that info is not dropped, and that none of the existing inaccuracies are made "worse".
There is more room to improve here, but would like to leave it at that for this PR. This method is used for various conversion kinds including user-defined conversions So I didn't find that adding an assertion that, say, only standard conversions are passed into this method, etc., was valid.
@dotnet/roslyn-compiler for review |
@@ -4613,7 +4613,11 @@ private static bool IsTargetTypedExpression(BoundExpression node) | |||
{ | |||
var (returnExpr, resultType, _) = returns[i]; | |||
resultTypes.Add(resultType); | |||
placeholdersBuilder.Add(CreatePlaceholderIfNecessary(returnExpr, resultType)); | |||
|
|||
if (!IsTargetTypedExpression(returnExpr)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The tests seem to focus on collection expressions as the "target-typed expression". Consider adding tests for the other kinds of target-typed expressions (null, default, switches, conditionals, lambdas, tuples, ...).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the scenario is about when:
- a target-typed expression is being converted to the result type of a best-type analysis (e.g.
b ? ["1"] : new[] { "2" }
), - and, there is a nested nullability reinference occurring in the other best-type candidate expressions, which we want to "win" over any initial-bound type which the target-typed expression may have.
So, there might be an interesting scenario to test like in b ? ("1", "2") : GenericMethod("1")
, or something like that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have added a test based on this suggestion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't follow. The test you added doesn't have lambdas.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are three places where an IsTargetTypedExpression
check was added: conditionals, lambdas and switches. The added test seems to cover conditionals.
Consider adding a similar test showing the effects of the fix on other target-typed expressions besides collection expressions in lambdas and switches.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Switches are covered in TargetTypedExpressions_NestedNullability
.
Lambdas are covered in CollectionExpression_NestedNullability_06
. Note that one of the cases involves collection-exprs, and the other case doesn't involve collection-exprs, but still shows an incorrect behavior, tracked in #74609
src/Compilers/CSharp/Test/Symbol/Symbols/Source/NullablePublicAPITests.cs
Show resolved
Hide resolved
var model = comp.GetSemanticModel(tree); | ||
|
||
var root = tree.GetRoot(); | ||
var collectionExpr = root.DescendantNodes().OfType<CollectionExpressionSyntax>().Single(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: just FYI in case you find it convenient, I added a test helper "GetSyntax" which allows GetSyntax<CollectionExpressionSyntax>(tree, """["1"]""")
to do the trick in this case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done with review pass (iteration 3). Only minor questions on tests
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM Thanks (iteration 10)
@@ -4613,7 +4613,11 @@ private static bool IsTargetTypedExpression(BoundExpression node) | |||
{ | |||
var (returnExpr, resultType, _) = returns[i]; | |||
resultTypes.Add(resultType); | |||
placeholdersBuilder.Add(CreatePlaceholderIfNecessary(returnExpr, resultType)); | |||
|
|||
if (!IsTargetTypedExpression(returnExpr)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's correct, but, I would prefer to leave this check in order to satisfy new assertions in BestTypeInferrer.
I didn't investigate closely but I am guessing something is causing the return type we wanted to infer here to be replaced unexpectedly with a type from initial binding.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually since this is only used in initial binding we might be able to remove the check and just assert that a target typed expr is not being used here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
urgh, this code probably would have behaved incorrectly if one of the exprs actually was target-typed, it expects that 'resultTypes ' and 'placeholdersBuilder' have the same count after the loop.
else if (IsTargetTypedExpression(alternative)) | ||
{ | ||
resultType = consequenceRValue.Type; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we testing this case?
#nullable enable | ||
|
||
var b = false; | ||
var arr = b ? ["1"] : new[] { "2" }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Closes #71522
Adjust NullableWalker to stop passing target-typed expressions to BestTypeInferrer. This was causing the converted collection-expr's type from initial binding to sneak into the nullable best type analysis, causing some NotAnnotated annotations to get replaced with Oblivious in the array element case.
Also adjust BestTypeInferrer to expect that target-typed expressions are not passed to it. It doesn't make sense for a target-typed expression to participate in best-type. Target-typed expressions should only be converted to the resulting best-type, not have their type used as input to best-type analysis.