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

Use flag to represent suppression in bound tree #32850

Merged
merged 10 commits into from
Jan 30, 2019
Merged

Conversation

jcouv
Copy link
Member

@jcouv jcouv commented Jan 27, 2019

This PR addresses some issues with the suppression operator. It does not address tuple scenarios or the semantic model.
Compared to #32699 which added many special cases based on bound node for suppression, this PR drops the suppression node from the bound tree and represents suppression as a flag.

Fixed a number of crashes
Fixes #29862 (top-level nullability of !)
Fixes #29903 (t! = s should be an error)
I also noticed that we skipped some warnings in cast scenarios (latest LDM notes on the topic)
Fixes #31294 (throw null!; isn't allowed)
Fixes #29905
Fixes #29906

Follow-ups:

Notes:

  • Although I ended up removing the IOperation test I'd added, I adjusted the comparison logic for IOperation assertions to tolerate whitespaces

@jcouv jcouv added this to the 16.1.P1 milestone Jan 27, 2019
@jcouv jcouv self-assigned this Jan 27, 2019
@jcouv jcouv requested review from a team as code owners January 27, 2019 02:37
if (unboundLambda.IsSuppressed)
{
boundLambda = boundLambda.WithSuppression();
}
Copy link
Member

@jaredpar jaredpar Jan 27, 2019

Choose a reason for hiding this comment

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

Is there a reason the implementation of Bind shouldn't be handling this? #Resolved

Copy link
Member Author

@jcouv jcouv Jan 28, 2019

Choose a reason for hiding this comment

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

That's a good idea. #Resolved

@@ -268,6 +270,14 @@ private BoundExpression BindArgListOperator(InvocationExpressionSyntax node, Dia
CheckRestrictedTypeReceiver(result, this.Compilation, diagnostics);

return result;

void reportSuppressionIfNeeded()
Copy link
Member

@jaredpar jaredpar Jan 27, 2019

Choose a reason for hiding this comment

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

reportSuppressionIfPresent? #Resolved

@@ -2041,7 +2041,17 @@ private BoundExpression BindIncrementOperator(CSharpSyntaxNode node, ExpressionS
private BoundExpression BindSuppressNullableWarningExpression(PostfixUnaryExpressionSyntax node, DiagnosticBag diagnostics)
{
var expr = BindExpression(node.Operand, diagnostics);
return new BoundSuppressNullableWarningExpression(node, expr, expr.Type);
switch (expr.Kind)
Copy link
Member

@jaredpar jaredpar Jan 27, 2019

Choose a reason for hiding this comment

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

Why not use IsLegalSuppressionValueKind here? Isn't that the same logic? #Resolved

Copy link
Member Author

@jcouv jcouv Jan 28, 2019

Choose a reason for hiding this comment

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

They are different logic.

Maybe IsLegalSuppressionValueKind is not the best name, but it checks which ValueKinds can be used to access a suppression expression.
For instance, ValueKind.IncrementDecrement is disallowed. That means we're blocking some cases where suppression could appear (x!++).

On the other hand, here, we're blocking what could appear underneath a suppression. #Resolved

@jaredpar
Copy link
Member

jaredpar commented Jan 27, 2019

            Diagnostic(ErrorCode.ERR_IllegalSuppression, "y").WithLocation(15, 20)

Why is this context illegal? I can imagine that ref returns will also hit nullable type mismatches #Resolved


Refers to: src/Compilers/CSharp/Test/Semantic/Semantics/NullableReferenceTypesTests.cs:47 in edd850f. [](commit_id = edd850f, deletion_comment = False)

@jaredpar
Copy link
Member

jaredpar commented Jan 27, 2019

Done with review pass (iteration 3). Really like this approach. I think it's much cleaner. #Resolved

@jcouv
Copy link
Member Author

jcouv commented Jan 28, 2019

Why is this context illegal? (return ref y!;) I can imagine that ref returns will also hit nullable type mismatches

I have a follow-up issue to refine the list of ValueKinds that can be used to access a suppressed value.
I agree the ref return should be allowed. I just didn't want to bloat this PR (especially in its initial form).

        private static bool IsLegalSuppressionValueKind(BindValueKind valueKind)
        {
            // Need to review allowed uses of the suppression operator
            // Tracked by https://github.com/dotnet/roslyn/issues/31297
``` #Resolved

Copy link
Member

@jaredpar jaredpar left a comment

Choose a reason for hiding this comment

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

:shipit:

}
else
{
Debug.Assert((_attributes & BoundNodeAttributes.IsSuppressed) == 0, "flag should not be reset");
Copy link
Member

@cston cston Jan 28, 2019

Choose a reason for hiding this comment

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

Debug.Assert((_attributes & BoundNodeAttributes.IsSuppressed) == 0, "flag should not be reset"); [](start = 20, length = 96)

Should we assert this always (before the if), to ensure the bit is not set twice? #Closed

{
if (expression.IsSuppressed)
{
Error(ErrorCode.ERR_ExpressionTreeCantContainSuppressNullableWarning, expression);
Copy link
Member

@cston cston Jan 28, 2019

Choose a reason for hiding this comment

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

ERR_ExpressionTreeCantContainSuppressNullableWarning [](start = 32, length = 52)

Why disallow ! in an expression tree?

Won't we report a nullability warning for => x?

    static void F(object? x)
    {
        Expression<Func<object>> e = () => x;
    }
``` #Closed

Copy link
Member Author

@jcouv jcouv Jan 28, 2019

Choose a reason for hiding this comment

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

When suppressions were represented as a bound node, I think it made more sense to disallow. Now I could go either way, but I'm still leaning on disallowing since it serves no purpose. That reserves our options in the future.

Why do you think we should allow?

Update: Chuck convinced me offline :-)


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

@@ -5317,7 +5314,11 @@ private void CheckPossibleNullReceiver(BoundExpression receiverOpt, bool checkNu
{
return;
}
ReportSafetyDiagnostic(isValueType ? ErrorCode.WRN_NullableValueTypeMayBeNull : ErrorCode.WRN_NullReferenceReceiver, syntaxOpt ?? receiverOpt.Syntax);

if (!receiverOpt.IsSuppressed)
Copy link
Member

@cston cston Jan 28, 2019

Choose a reason for hiding this comment

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

if (!receiverOpt.IsSuppressed) [](start = 20, length = 30)

Is this check necessary? Won't _resultType have been set appropriately by the caller? #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.

Thanks. Indeed. I think I added this before I fixed the result type from VisitExpressionWithoutStackGuard and should have revisited this afterwards.


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

@@ -238,6 +238,7 @@ protected ConstantValue MakeDefaultExpression(DiagnosticBag diagnostics, Binder
parameterType.NullableAnnotation.IsAnyNotNullable() &&
convertedExpression.ConstantValue?.IsNull == true &&
!suppressNullableWarning(convertedExpression) &&
!convertedExpression.IsSuppressed &&
Copy link
Member

@cston cston Jan 28, 2019

Choose a reason for hiding this comment

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

convertedExpression.IsSuppressed [](start = 17, length = 32)

Should this be handled in suppressNullableWarning()? #Closed

return (_attributes & BoundNodeAttributes.IsSuppressed) != 0;
}
internal set
{
Copy link
Member

@gafter gafter Jan 28, 2019

Choose a reason for hiding this comment

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

The existence of this internal setter is inherently dangerous. Can you make it protected? Or better yet remove it and instead add a protected method whose purpose is to set suppressed to true. #Resolved

@gafter
Copy link
Member

gafter commented Jan 28, 2019

internal sealed partial class BoundQueryClause : BoundExpression

BoundQueryClause has three fields declared in BoundQueryClause.cs. Does that need to be handled? #Resolved


Refers to: src/Compilers/CSharp/Portable/Generated/BoundNodes.xml.Generated.cs:8064 in eaa8738. [](commit_id = eaa8738, deletion_comment = False)

@@ -69,6 +69,13 @@ public BoundLambda(SyntaxNode syntax, UnboundLambda unboundLambda, BoundBlock bo
);
}

public BoundLambda WithSuppression()
Copy link
Member

@gafter gafter Jan 28, 2019

Choose a reason for hiding this comment

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

WithSuppression [](start = 27, length = 15)

This method does not override anything virtual. Where is it called?

I recommend making WithSuppression a virtual method in BoundExpression (rather than an extension method) so that it can be overridden.

Structured that way, you might consider renaming WithSuppressionCore to ShallowClone and move the setting of the suppression bit to the WithSuppression method. #Resolved

{
Error(ErrorCode.ERR_ExpressionTreeCantContainRefStruct, node, type.Name);
ReportSuppressionIfNeeded(expression);

Copy link
Member

@gafter gafter Jan 28, 2019

Choose a reason for hiding this comment

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

I don't see a problem with suppression in expression trees. Like parens, they would be applied at compile-time only. #Resolved

{
return (T)node.WithSuppressionCore();
}

Copy link
Member

@gafter gafter Jan 28, 2019

Choose a reason for hiding this comment

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

Does any caller need this to be generic? #Resolved

@@ -1594,6 +1645,11 @@ private static bool SkipInVisitor(Field f)
return f.SkipInVisitor != null && string.Compare(f.SkipInVisitor, "true", true) == 0;
}

private static bool SkipShallowClone(Node n)
{
return n.SkipShallowClone != null && string.Compare(n.SkipShallowClone, "true", true) == 0;
Copy link
Member

@cston cston Jan 29, 2019

Choose a reason for hiding this comment

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

The n.SkipShallowClone != null check seems unnecessary. (Same comment for similar existing methods above.) #Closed

@cston
Copy link
Member

cston commented Jan 29, 2019

}

Consider testing nameof(a!) as well. #Resolved


Refers to: src/Compilers/CSharp/Test/Semantic/Semantics/NullableReferenceTypesTests.cs:414 in 5bc7a16. [](commit_id = 5bc7a16, deletion_comment = False)

@@ -885,11 +885,49 @@ private void WriteType(TreeType node)
{
WriteAccept(node.Name);
WriteUpdateMethod(node as Node);
WriteWithSuppressionMethodIfNeeded(node as Node);
Copy link
Member

@gafter gafter Jan 29, 2019

Choose a reason for hiding this comment

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

WriteWithSuppressionMethodIfNeeded [](start = 16, length = 34)

Should be WriteShallowCloneMethodIfNeeded #Resolved

@jcouv
Copy link
Member Author

jcouv commented Jan 29, 2019

Consider testing nameof(a!) as well.

That's covered in SuppressNullableWarning_MemberAccess and SymbolInfoForMethodGroup03 #Resolved

@cston
Copy link
Member

cston commented Jan 29, 2019

            Assert.True(expectedType == actualType, $"Unexpected TypeInfo.Type '{actualType}'");

Consider using Assert.Equal(expectedType, actualType) here and below since Assert.Equal() reports both expected and actual values. #WontFix


Refers to: src/Compilers/CSharp/Test/Semantic/Semantics/NullableReferenceTypesTests.cs:515 in 5bc7a16. [](commit_id = 5bc7a16, deletion_comment = False)

@cston
Copy link
Member

cston commented Jan 29, 2019

}";

Consider testing:

static void Main()
{
    _ = Main()!;
}
``` #Closed

---
Refers to: src/Compilers/CSharp/Test/Semantic/Semantics/NullableReferenceTypesTests.cs:1476 in 5bc7a16. [](commit_id = 5bc7a161b6fc688e8b728d306f9afaa45e838674, deletion_comment = False)

@cston
Copy link
Member

cston commented Jan 29, 2019

        CreateCompilation(text).VerifyDiagnostics(

Please include comments. #Closed


Refers to: src/Compilers/CSharp/Test/Semantic/Semantics/NullableReferenceTypesTests.cs:1509 in 5bc7a16. [](commit_id = 5bc7a16, deletion_comment = False)

@cston
Copy link
Member

cston commented Jan 29, 2019

}

Consider reducing down to:

static void Main()
{
    var x = (() => { return 1; } !) ?.ToString();
} #Closed

---
Refers to: src/Compilers/CSharp/Test/Semantic/Semantics/NullableReferenceTypesTests.cs:1537 in 5bc7a16. [](commit_id = 5bc7a161b6fc688e8b728d306f9afaa45e838674, deletion_comment = False)

@cston
Copy link
Member

cston commented Jan 29, 2019

/*<bind>*/{

Not used. #Closed


Refers to: src/Compilers/CSharp/Test/Semantic/Semantics/NullableReferenceTypesTests.cs:1913 in 5bc7a16. [](commit_id = 5bc7a16, deletion_comment = False)

(type.IsNull ? "<null>" : type.ToDisplayString(TypeSymbolWithAnnotations.TestDisplayFormat)) :
null;
// We don't support VerifyTypes on suppressions at the moment
Debug.Assert(syntaxOpt.Kind() != SyntaxKind.SuppressNullableWarningExpression);
Copy link
Member

@cston cston Jan 29, 2019

Choose a reason for hiding this comment

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

Assert.NotEqual(syntaxOpt.Kind(), SyntaxKind.SuppressNullableWarningExpression); #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.

This was intentional. It's documenting the contract of the local function, rather than a test assertion. But I don't mind either way...


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

@jcouv
Copy link
Member Author

jcouv commented Jan 29, 2019

            Assert.True(expectedType == actualType, $"Unexpected TypeInfo.Type '{actualType}'");

This is on purpose because I found the test output more useful this way. I don't like to click on the stack trace to figure out if the problem is with Type or ConvertedType.
Unfortunately, Assert.Equal doesn't allow for a message...


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


Refers to: src/Compilers/CSharp/Test/Semantic/Semantics/NullableReferenceTypesTests.cs:515 in 5bc7a16. [](commit_id = 5bc7a16, deletion_comment = False)

@@ -100,6 +100,20 @@ public SyntaxTree SyntaxTree
}
}

protected void CopyAttributes(BoundNode original)
{
if (this.WasCompilerGenerated)
Copy link
Member

@cston cston Jan 29, 2019

Choose a reason for hiding this comment

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

Shouild this be if (original.WasCompilerGenerated)? #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.

D'uh. Sorry :-(


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

if (original.IsSuppressed)
{
this.IsSuppressed = original.IsSuppressed;
}
Copy link
Member

@cston cston Jan 29, 2019

Choose a reason for hiding this comment

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

I realize each attribute checks the value is not already set, but ignoring that, it seems strange that the contract of this method is to only set the attributes if the original attributes were set.

Consider setting the attributes unconditionally:

this.WasCompilerGenerated = original.WasCompilerGenerated;
this.IsSuppressed = original.IsSuppressed;
``` #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.

What about VB?
I tried to align the two implementations since it is possible.


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

Copy link
Member

Choose a reason for hiding this comment

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

The two implementations will be distinct since VB uses SetWasCompilerGenerated() that does not allow setting False.


In reply to: 252049465 [](ancestors = 252049465,252046440)

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 C# WasCompilerGenerated setter also does not allow setting to false


In reply to: 252051070 [](ancestors = 252051070,252049465,252046440)

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.

5 participants