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
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2661,5 +2661,53 @@ class C
return x;
}
}");

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

@"#nullable enable

using System;

class C
{
public string? M()
{
[|string? x = null;
Func<string?> returnNull = () =>
{
return null;
};

x = returnNull() ?? string.Empty;|]

return x;
}
}",
@"#nullable enable

using System;

class C
{
public string? M()
{
string? x = {|Rename:NewMethod|}();

return x;
}

private static string NewMethod()
{
string? x = null;
Func<string?> returnNull = () =>
{
return null;
};

x = returnNull() ?? string.Empty;
return x;
}
}");
}
jasonmalinowski marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
Expand Up @@ -147,37 +147,42 @@ protected override ITypeSymbol GetSymbolType(SemanticModel semanticModel, ISymbo
var references = selectionOperation.DescendantsAndSelf()
.Where(IsSymbolReferencedByOperation);

var flowState = GetFlowStateFromLocations(references);
return base.GetSymbolType(semanticModel, symbol).WithNullability(flowState);
if (AreAllReferencesNotNull(references))
{
return base.GetSymbolType(semanticModel, symbol).WithNullability(NullableAnnotation.NotAnnotated);
}

return base.GetSymbolType(semanticModel, symbol);

default:
return base.GetSymbolType(semanticModel, symbol);
}

NullableFlowState GetFlowStateFromLocations(IEnumerable<IOperation> references)
bool AreAllReferencesNotNull(IEnumerable<IOperation> references)
ryzngard marked this conversation as resolved.
Show resolved Hide resolved
{
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?


switch (typeInfo.Nullability.FlowState)
ryzngard marked this conversation as resolved.
Show resolved Hide resolved
{
case NullableFlowState.MaybeNull: return NullableFlowState.MaybeNull;
case NullableFlowState.None: return NullableFlowState.None;
case NullableFlowState.MaybeNull:
case NullableFlowState.None:
return false;
}
}

return NullableFlowState.NotNull;
return true;
}

bool IsSymbolReferencedByOperation(IOperation operation)
=> operation switch
{
ILocalReferenceOperation localReference => localReference.Local == symbol,
IParameterReferenceOperation parameterReference => parameterReference.Parameter == symbol,
IAssignmentOperation assignment => IsSymbolReferencedByOperation(assignment.Target),
_ => false
};
=> operation switch
{
ILocalReferenceOperation localReference => localReference.Local == symbol,
ryzngard marked this conversation as resolved.
Show resolved Hide resolved
IParameterReferenceOperation parameterReference => parameterReference.Parameter == symbol,
IAssignmentOperation assignment => IsSymbolReferencedByOperation(assignment.Target),
_ => false
};
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,11 @@ protected override async Task<SemanticDocument> UpdateMethodAfterGenerationAsync

foreach (var returnOperation in returnOperations)
{
if (!ReturnOperationBelongsToMethod(returnOperation.Syntax, methodOperation.Syntax))
ryzngard marked this conversation as resolved.
Show resolved Hide resolved
{
continue;
}

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 returnTypeInfo = semanticModel.GetTypeInfo(syntax, cancellationToken);
if (returnTypeInfo.Nullability.FlowState == NullableFlowState.MaybeNull)
Expand All @@ -694,6 +699,19 @@ protected override async Task<SemanticDocument> UpdateMethodAfterGenerationAsync

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.


static bool ReturnOperationBelongsToMethod(SyntaxNode returnOperationSyntax, SyntaxNode methodSyntax)
{
var enclosingMethod = returnOperationSyntax.FirstAncestorOrSelf<SyntaxNode>(n => n switch
{
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.

_ => false
});

return enclosingMethod == methodSyntax;
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ private void WrapReturnTypeInTask(SemanticModel model, ref ITypeSymbol returnTyp
{
// check whether current selection contains return statement
var parameters = GetMethodParameters(variableInfoMap.Values);
var returnType = GetReturnTypeFromStatement(model);
var returnType = SelectionResult.GetContainingScopeType() ?? compilation.GetSpecialType(SpecialType.System_Object);

var unsafeAddressTakenUsed = ContainsVariableUnsafeAddressTaken(dataFlowAnalysisData, variableInfoMap.Keys);
return (parameters, returnType, default(VariableInfo), unsafeAddressTakenUsed);
Expand All @@ -231,23 +231,14 @@ private void WrapReturnTypeInTask(SemanticModel model, ref ITypeSymbol returnTyp
var parameters = MarkVariableInfoToUseAsReturnValueIfPossible(GetMethodParameters(variableInfoMap.Values));
var variableToUseAsReturnValue = parameters.FirstOrDefault(v => v.UseAsReturnValue);
var returnType = variableToUseAsReturnValue != null
? GetReturnTypeFromReturnVariable(_semanticDocument, variableToUseAsReturnValue)
? variableToUseAsReturnValue.GetVariableType(_semanticDocument)
: compilation.GetSpecialType(SpecialType.System_Void);

var unsafeAddressTakenUsed = ContainsVariableUnsafeAddressTaken(dataFlowAnalysisData, variableInfoMap.Keys);
return (parameters, returnType, variableToUseAsReturnValue, unsafeAddressTakenUsed);
}
}

protected ITypeSymbol GetReturnTypeFromStatement(SemanticModel semanticModel)
{
var compilation = semanticModel.Compilation;
return SelectionResult.GetContainingScopeType() ?? compilation.GetSpecialType(SpecialType.System_Object);
}

protected ITypeSymbol GetReturnTypeFromReturnVariable(SemanticDocument semanticDocument, VariableInfo variableToUseAsReturnValue)
=> variableToUseAsReturnValue.GetVariableType(semanticDocument);

private bool IsInExpressionOrHasReturnStatement(SemanticModel model)
{
var isInExpressionOrHasReturnStatement = SelectionResult.SelectionInExpression;
Expand Down