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

Add nullability information to extract method #37851

Merged
merged 22 commits into from
Aug 22, 2019

Conversation

ryzngard
Copy link
Contributor

@ryzngard ryzngard commented Aug 8, 2019

The general premise is to favor non nullable reference types when possible, but we need to use flow analysis to be sure.

  • Use analysis on all parameters to determine if they are ever assigned to null (if a ref parameter)
  • Analyze if a parameter is never null in reference locations
  • Analyze all returns of a generated method to determine if null is possible
  • Add, hopefully, enough comprehensive tests to cover a wide array of possibilities

@ryzngard ryzngard force-pushed the issue/extract_method_nullability branch from 4031564 to 56f48e2 Compare August 13, 2019 00:01
@ryzngard ryzngard marked this pull request as ready for review August 13, 2019 00:01
@ryzngard ryzngard requested a review from a team as a code owner August 13, 2019 00:01
Copy link
Member

@jasonmalinowski jasonmalinowski left a comment

Choose a reason for hiding this comment

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

Bunch of nitpick questions or things that looked like you went one way and then decided to go another way.

{
foreach (var reference in references)
{
var typeInfo = semanticModel.GetTypeInfo(reference.Syntax);
Copy link
Member

Choose a reason for hiding this comment

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

In the assignment case, is this the flow state of the left hand or right hand side? Does it matter?

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 guessing it's left hand, but I'm not really sure. @333fred ?

Copy link
Member

Choose a reason for hiding this comment

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

Flow state is always RHS, Annotations are always LHS. If I understand your question.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So in the case of assignment, we're looking to see if it ever can be assigned to null. That makes sense then, we want the RHS.

Copy link
Member

Choose a reason for hiding this comment

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

I guess the question was "if you ask for the flow state of foo" in "foo = bar", is that the flow state after the assignment to bar (so it would have the same flow state of bar) or is that the flow state before the assignment of bar?

@CyrusNajmabadi
Copy link
Member

Add, hopefully, enough comprehensive tests to cover a wide array of possibilities

👍

var newRoot = oldRoot.ReplaceNode(methodDeclaration, newMethodDeclaration);

var newDocument = originalDocument.Document.WithSyntaxRoot(newRoot);
return await SemanticDocument.CreateAsync(newDocument, cancellationToken).ConfigureAwait(false);
Copy link
Member

Choose a reason for hiding this comment

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

this fixing up "after the fact" seems super hinky and error prone. is there a reason we can't generate it with the right nullability right from the start?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So I tried to make this work in generating the right nullability annotation and I found it too difficult since we can modify the code by adding return statements. I ended up basically writing a predictive flow analyzer for all the cases we could have to deal with where we would inject a return statement. I'm time boxing to the current design of fixing up after the fact, and I'll do my best to put a reasonable explanation as a comment here as to why it's this way. The only option I see is to accept the annotated nullability as desired and move on, but we asserted that we would like to be smarter than that.

I'm very much open to ideas and discussion on this, but I think this is the most robust solution at this time based on my investigation.

Copy link
Member

Choose a reason for hiding this comment

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

The nullability project has a "IDE cleanup" column to track debt we created; this might be worth an entry there.

Copy link
Member

@jasonmalinowski jasonmalinowski left a comment

Choose a reason for hiding this comment

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

A few remaining questions; happy to sign off assuming they're either answered or we've appropriately tracked any deferred work.


foreach (var returnOperation in returnOperations)
{
var syntax = returnOperation.ReturnedValue?.Syntax ?? returnOperation.Syntax;
Copy link
Member

Choose a reason for hiding this comment

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

Is this going to blow up if we have a return; inside a method that is non-void-returning?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not exactly. We end up not modifying the return type because it reports as None for flow state

var newRoot = oldRoot.ReplaceNode(methodDeclaration, newMethodDeclaration);

var newDocument = originalDocument.Document.WithSyntaxRoot(newRoot);
return await SemanticDocument.CreateAsync(newDocument, cancellationToken).ConfigureAwait(false);
Copy link
Member

Choose a reason for hiding this comment

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

The nullability project has a "IDE cleanup" column to track debt we created; this might be worth an entry there.

// follow our recommendation of only using nullable when necessary.
// This is done after method generation instead of at analyzer time because it's purely
// based on the resulting code, which the generator can modify as needed. If return statements
// are added, the flow analysis could change to indicate something different. It's cleaner to rely
Copy link
Member

Choose a reason for hiding this comment

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

I'm still not sure I understand the case where that would happen. That would seem like the transformation we're doing in extract method has some deeper issue.


[Fact, Trait(Traits.Feature, Traits.Features.CodeActionsExtractMethod)]
public Task TestFlowNullable_LambdaWithReturn()
=> TestInRegularAndScriptAsync(
Copy link
Member

Choose a reason for hiding this comment

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

in a future PR, could you update everything to be consistent 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.

Naming style, or the expression body style?

Copy link
Member

Choose a reason for hiding this comment

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

expression-body style.

{
BaseMethodDeclarationSyntax _ => true,
AnonymousFunctionExpressionSyntax _ => true,
LocalFunctionStatementSyntax _ => true,
Copy link
Member

Choose a reason for hiding this comment

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

i feel like there's are ISyntaxFacts for this...

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 didn't see one. I saw for method body, and a few that take a position for method head.

Copy link
Member

@jasonmalinowski jasonmalinowski left a comment

Choose a reason for hiding this comment

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

Remaining comments may be addressed after merge.

}

bool AreAllReferencesNotNull(IEnumerable<IOperation> references)
=> references.All(r => semanticModel.GetTypeInfo(r.Syntax).Nullability.FlowState == NullableFlowState.NotNull);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
=> references.All(r => semanticModel.GetTypeInfo(r.Syntax).Nullability.FlowState == NullableFlowState.NotNull);
=> references.All(r => semanticModel.GetTypeInfo(r.Syntax, cancellationToken).Nullability.FlowState == NullableFlowState.NotNull);

Copy link
Member

Choose a reason for hiding this comment

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

(which will also need it to be passed in)


foreach (var returnOperation in returnOperations)
{
// If thereturn statement is located in a nested local function or lambda it
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// If thereturn statement is located in a nested local function or lambda it
// If the return statement is located in a nested local function or lambda it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants