Skip to content

Enable Nullability Information in IOperation.Type#42403

Merged
13 commits merged intorelease/dev16.8-preview1from
iop-nullable
Jun 24, 2020
Merged

Enable Nullability Information in IOperation.Type#42403
13 commits merged intorelease/dev16.8-preview1from
iop-nullable

Conversation

@333fred
Copy link
Member

@333fred 333fred commented Mar 14, 2020

This enables nullability information from rewritten BoundNodes
in IOperation, exposed via the NullableAnnotation property on
IOperation.Type. The bulk of the change is creating a simple helper
on BoundExpression to return the correctly annotated ITypeSymbol,
and using that in the factory instead of .Type.GetPublicSymbol. This
exposed another issue with a few of our tests though: in DEBUG mode,
when nullable is gobally disabled, we perform a full nullable analysis
and rewrite and then discard the results, to ensure that we don't
crash and to run our general verification code. However, there are
a few nodes that get cached by binders (such as LockOrUsingBinder).
These nodes do not get rewritten as their constituent elements did
not change, so when we set lazy properties like TopLevelNullability
info, the cache from the throwaway binding gets updated as well.
To remedy this and still get coverage, we pass along a flag to abort
before the actual rewrite in DEBUG scenarios. This fixes #40352

@jcouv jcouv added the PR For Personal Review Only The PR doesn’t require anyone other than the developer to review it. label Mar 18, 2020
@333fred 333fred marked this pull request as ready for review March 30, 2020 22:16
@333fred 333fred requested a review from a team as a code owner March 30, 2020 22:16
@333fred 333fred requested a review from AlekseyTs March 30, 2020 22:16
@333fred 333fred removed the PR For Personal Review Only The PR doesn’t require anyone other than the developer to review it. label Mar 30, 2020
@333fred
Copy link
Member Author

333fred commented Mar 30, 2020

@dotnet/roslyn-compiler @AlekseyTs This is now ready for review. My testing strategy here was to update everywhere that we are currently testing the nullable api to get the IOperation tree for that location, and assert that it matches the info from GetTypeInfo.

/cc @CyrusNajmabadi @dpoeschl

@333fred
Copy link
Member Author

333fred commented Mar 31, 2020

@dotnet/roslyn-compiler @AlekseyTs for a review please.

out SnapshotManager? snapshotManager,
ref ImmutableDictionary<Symbol, Symbol>? remappedSymbols)
ref ImmutableDictionary<Symbol, Symbol>? remappedSymbols,
bool avoidRewrite)
Copy link
Member

@jaredpar jaredpar Mar 31, 2020

Choose a reason for hiding this comment

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

The prefix avoid does not line up with the documentation that says "force". Feel like one of those needs to change. #Resolved

Copy link
Member Author

Choose a reason for hiding this comment

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

Splitting this up.


In reply to: 401064745 [](ancestors = 401064745)

#if DEBUG
Debug.Assert(avoidRewrite != compilation.NullableSemanticAnalysisEnabled);
#else
if (avoidRewrite) throw ExceptionUtilities.Unreachable;
Copy link
Member

@jaredpar jaredpar Mar 31, 2020

Choose a reason for hiding this comment

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

Overall this dance feels a bit awkward. We're using runtime checks to enforce behavior that is actually codified at compile time. I think this would be a lot cleaner if we split it up into two methods: Analyze and AnalyzeAndRewrite. #Resolved


if (avoidRewrite)
{
return node;
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 3, 2020

Choose a reason for hiding this comment

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

return node [](start = 16, length = 11)

return node [](start = 16, length = 11)

Would it make sense to push this down to the Rewrite to disable actual node modifications, but do all the other work? #Closed

Copy link
Member Author

Choose a reason for hiding this comment

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

Responded in the other comment, but I don't believe that running the rewriter actually gets us any real verification and it would significantly complicate the generated code.


In reply to: 402669321 [](ancestors = 402669321)

// in the IOperation tree, and we should retrieve the IOperation node underlying the suppression.
if (expression.IsKind(SyntaxKind.SuppressNullableWarningExpression))
{
expression = ((PostfixUnaryExpressionSyntax)expression).Operand;
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 3, 2020

Choose a reason for hiding this comment

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

expression = ((PostfixUnaryExpressionSyntax)expression).Operand; [](start = 20, length = 64)

expression = ((PostfixUnaryExpressionSyntax)expression).Operand; [](start = 20, length = 64)

Should we do this in a loop and, perhaps, dig through parenthesis too? #Closed

Copy link
Member Author

Choose a reason for hiding this comment

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

I believe this was originally in getOperation, I am proceeding with that assumption.


In reply to: 402673404 [](ancestors = 402673404)

Copy link
Contributor

@AlekseyTs AlekseyTs left a comment

Choose a reason for hiding this comment

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

LGTM (iteartion 6)

@333fred 333fred marked this pull request as draft June 10, 2020 23:41

static bool shouldAvoidRewrite(CSharpCompilation comp) =>
#if DEBUG
!comp.NullableSemanticAnalysisEnabled;
Copy link
Member Author

@333fred 333fred Jun 10, 2020

Choose a reason for hiding this comment

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

Putting this comment back in line after rebasing:

Doesn't this disable a good chunk of testing we were trying to do in DEBUG? #Resolved

Copy link
Member Author

@333fred 333fred Jun 10, 2020

Choose a reason for hiding this comment

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

The only portion that is disabled is the actual rewrite step. I does technically add more coverage, but in practice I don't believe that's a huge gain. The rewriter is almost entirely generated and does very little validation.


In reply to: 438466952 [](ancestors = 438466952)

Assert.Equal("System.String?", model.GetTypeInfo(declaration.Type).ConvertedType.ToTestDisplayString());
Assert.Equal(CodeAnalysis.NullableAnnotation.Annotated, model.GetTypeInfo(declaration.Type).ConvertedNullability.Annotation);
Assert.Equal("System.String?", model.GetTypeInfoAndVerifyIOperation(declaration.Type).Type.ToTestDisplayString());
Assert.Equal(CodeAnalysis.NullableAnnotation.Annotated, model.GetTypeInfoAndVerifyIOperation(declaration.Type).Nullability.Annotation);
Copy link
Member Author

@333fred 333fred Jun 11, 2020

Choose a reason for hiding this comment

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

GetTypeInfoAndVerifyIOperation [](start = 74, length = 30)

Putting this comment back in line after rebase:

Perhaps the second time we can skip IOperation check? There are several more similar cases like this. #Resolved

@333fred 333fred marked this pull request as ready for review June 11, 2020 23:51
@333fred
Copy link
Member Author

333fred commented Jun 11, 2020

@AlekseyTs @jaredpar this is ready for another review.

@333fred
Copy link
Member Author

333fred commented Jun 12, 2020

When I have sign-offs on this change I'm going to change the base branch to be 16.8 as this doesn't meant the 16.7 bar. Keeping this in master for now though so review comments don't jump.

@333fred
Copy link
Member Author

333fred commented Jun 12, 2020

@AlekseyTs @jaredpar for another review please.

@333fred 333fred requested a review from AlekseyTs June 12, 2020 23:09
@333fred
Copy link
Member Author

333fred commented Jun 15, 2020

@AlekseyTs @jaredpar for another review please.

return null;
}

public override BoundNode? VisitSwitchExpressionArm(BoundSwitchExpressionArm node)
Copy link
Contributor

@AlekseyTs AlekseyTs Jun 16, 2020

Choose a reason for hiding this comment

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

VisitSwitchExpressionArm [](start = 39, length = 24)

What is the actual change in behavior for a switch expression here? What does base implementation do differently and why is that a problem? #Resolved

Copy link
Member Author

Choose a reason for hiding this comment

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

The base unconditionally visits the when clause. This is an issue for the reason in the comment.


In reply to: 441175291 [](ancestors = 441175291)

@333fred
Copy link
Member Author

333fred commented Jun 18, 2020

@dotnet/roslyn-compiler for a second review.

if (node.Left is BoundObjectInitializerMember { MemberSymbol: null })
{
VerifyExpression(node);
return Visit(node.Left);
Copy link
Contributor

@AlekseyTs AlekseyTs Jun 18, 2020

Choose a reason for hiding this comment

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

return Visit(node.Left); [](start = 20, length = 24)

This looks suspicious. Why are we replacing an assignment with its left side? If return value is not important, let's return null. #Pending

@333fred
Copy link
Member Author

333fred commented Jun 19, 2020

@dotnet/roslyn-compiler for a second review.

@333fred
Copy link
Member Author

333fred commented Jun 19, 2020

@dotnet/roslyn-compiler for a second review please.

@333fred
Copy link
Member Author

333fred commented Jun 22, 2020

@dotnet/roslyn-compiler for another review.

/// Performs just the analysis step of getting nullability information for a semantic model. This is only used during DEBUG builds where nullable
/// is turned off on a compilation level, giving some extra debug verification of the nullable flow analysis.
/// </summary>
protected abstract void AnalyzeBoundNodeNullability(BoundNode boundRoot, Binder binder, DiagnosticBag diagnostics, bool takeSnapshots);
Copy link
Member

@jaredpar jaredpar Jun 23, 2020

Choose a reason for hiding this comment

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

The declaration of this method has the parameter name takeSnapshots but all of the overrides have the parameter craeteSnapshots. Feel like we should be consistent here. #Pending

#if DEBUG
if (!Compilation.NullableSemanticAnalysisEnabled)
{
AnalyzeBoundNodeNullability(boundRoot, binder, diagnosticBag, takeSnapshots: true);
Copy link
Member

@jaredpar jaredpar Jun 23, 2020

Choose a reason for hiding this comment

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

I understand that diagnosticBag here will be a freshly created bag whenever we hit this code. This still feels very indirect though. Have to think a bit to verify this code isn't potentially adding new diagnostics in DEBUG mode. Is there a reason to not just use new DiagnosticBag() here to make this clearer? #ByDesign

Copy link
Member Author

Choose a reason for hiding this comment

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

I wanted to avoid having the debug verification step create multiple throwaway diagnostic bags.


In reply to: 443950238 [](ancestors = 443950238)

333fred added 12 commits June 23, 2020 09:59
This enables nullability information from rewritten BoundNodes
in IOperation, exposed via the NullableAnnotation property on
IOperation.Type. The bulk of the change is creating a simple helper
on BoundExpression to return the correctly annotated ITypeSymbol,
and using that in the factory instead of .Type.GetPublicSymbol. This
exposed another issue with a few of our tests though: in DEBUG mode,
when nullable is gobally disabled, we perform a full nullable analysis
and rewrite and then discard the results, to ensure that we don't
crash and to run our general verification code. However, there are
a few nodes that get cached by binders (such as LockOrUsingBinder).
These nodes do not get rewritten as their constituent elements did
not change, so when we set lazy properties like TopLevelNullability
info, the cache from the throwaway binding gets updated as well.
To remedy this and still get coverage, we pass along a flag to abort
before the actual rewrite in DEBUG scenarios.
…s are an rvalue context). Add testing to everywhere we're currently testing GetTypeInfo with nullable enabled. Enable digging through nullable suppression operators like we do for GetTypeInfo.
* Split debug verification out into AnalyzeWithoutRewrite to simplify code.
* Add additional GetPublicTypeSymbol calls in OperationFactory with changes in master.
* Add a few NullableWalker.DebugVerifier cases for bugs.
…ssions when validating IOperation nullability.
@333fred 333fred requested review from a team as code owners June 23, 2020 17:00
@333fred 333fred changed the base branch from master to release/dev16.8-preview1 June 23, 2020 17:00
Copy link

@ghost ghost left a comment

Choose a reason for hiding this comment

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

Auto-approval

Copy link
Member

@gafter gafter left a comment

Choose a reason for hiding this comment

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

😷

@ghost ghost merged commit 47e6ea9 into release/dev16.8-preview1 Jun 24, 2020
@ghost ghost deleted the iop-nullable branch June 24, 2020 00:09
This pull request was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Nullability.FlowState returned for a test expression of a switch statement differs DEBUG vs. RELEASE

5 participants