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

Fix nested nullability of collection expressions #74490

Merged
merged 11 commits into from
Aug 15, 2024

Conversation

RikkiGibson
Copy link
Contributor

@RikkiGibson RikkiGibson commented Jul 22, 2024

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.

@dotnet-issue-labeler dotnet-issue-labeler bot added Area-Compilers untriaged Issues and PRs which have not yet been triaged by a lead labels Jul 22, 2024
// 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);
Copy link
Contributor Author

@RikkiGibson RikkiGibson Jul 30, 2024

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

Copy link
Member

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?

Copy link
Contributor Author

@RikkiGibson RikkiGibson Aug 9, 2024

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.

@jcouv jcouv self-assigned this Jul 30, 2024
@RikkiGibson RikkiGibson changed the title Fix nested nullability of collection expressions in public APIs Fix nested nullability of collection expressions Jul 30, 2024
@RikkiGibson RikkiGibson marked this pull request as ready for review July 30, 2024 22:45
@RikkiGibson RikkiGibson requested a review from a team as a code owner July 30, 2024 22:45
@RikkiGibson
Copy link
Contributor Author

@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))
Copy link
Member

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, ...).

Copy link
Contributor Author

@RikkiGibson RikkiGibson Aug 5, 2024

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:

  1. a target-typed expression is being converted to the result type of a best-type analysis (e.g. b ? ["1"] : new[] { "2" }),
  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.

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Member

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.

Copy link
Contributor Author

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

var model = comp.GetSemanticModel(tree);

var root = tree.GetRoot();
var collectionExpr = root.DescendantNodes().OfType<CollectionExpressionSyntax>().Single();
Copy link
Member

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.

Copy link
Member

@jcouv jcouv left a 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

Copy link
Member

@jcouv jcouv left a 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)

@RikkiGibson RikkiGibson requested a review from a team August 9, 2024 21:44
@@ -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))
Copy link
Member

Choose a reason for hiding this comment

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

if (!IsTargetTypedExpression(returnExpr))

Is this change covered by a test? It didn't seem like the new tests were affected by this check.

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'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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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;
}
Copy link
Member

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" };
Copy link
Member

Choose a reason for hiding this comment

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

var arr = b ? ["1"] : new[] { "2" };

Consider also testing:

arr = b ? new[] { "1" } : ["2"];

@RikkiGibson RikkiGibson merged commit e041433 into dotnet:main Aug 15, 2024
24 checks passed
@RikkiGibson RikkiGibson deleted the issue71522 branch August 15, 2024 00:41
@dotnet-policy-service dotnet-policy-service bot added this to the Next milestone Aug 15, 2024
@dibarbet dibarbet modified the milestones: Next, 17.12 P2 Aug 26, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-Compilers untriaged Issues and PRs which have not yet been triaged by a lead
Projects
None yet
Development

Successfully merging this pull request may close these issues.

IDE0300 fix-all skips first diagnostic in a cascading ternary
4 participants