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

Track nullability of nullable value types #31499

Merged
merged 30 commits into from
Dec 11, 2018
Merged

Track nullability of nullable value types #31499

merged 30 commits into from
Dec 11, 2018

Conversation

cston
Copy link
Member

@cston cston commented Dec 3, 2018

No description provided.

@cston cston requested a review from a team as a code owner December 3, 2018 18:45
@@ -835,9 +835,10 @@ private void CheckNewModifier(Symbol symbol, bool isNew, DiagnosticBag diagnosti
for (int i = 0; i < overridingParameters.Length; i++)
Copy link
Member

@333fred 333fred Dec 3, 2018

Choose a reason for hiding this comment

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

Nit: this whole loop can just be converted to a foreach, we don't use i anywhere except accessing overridingParameters. #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.

Unfortunately i is used for overriddenParameters as well as overridingParameters.


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

Copy link
Member

Choose a reason for hiding this comment

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

Ah dang, and I thought I had gone through thoroughly here to avoid looking blind.


In reply to: 238500830 [](ancestors = 238500830,238440431)

TypeCompareKind.AllIgnoreOptions & ~(TypeCompareKind.IgnoreNullableModifiersForReferenceTypes | TypeCompareKind.IgnoreInsignificantNullableModifiersDifference)) &&
overridingParameters[i].Type.Equals(overridenParameterType, TypeCompareKind.AllIgnoreOptions))
overridingParameterType.Equals(overridenParameterType, TypeCompareKind.AllIgnoreOptions))
{
diagnostics.Add(ErrorCode.WRN_NullabilityMismatchInParameterTypeOnOverride, overridingMemberLocation, new FormattedSymbol(overridingParameters[i], SymbolDisplayFormat.ShortFormat));
Copy link
Member

@333fred 333fred Dec 3, 2018

Choose a reason for hiding this comment

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

overridingParameters[i] [](start = 158, length = 23)

If you don't want to update the whole loop, please at least do this instance. #ByDesign

@@ -16498,9 +16498,15 @@ static void F6(A<object?>? x6, B<object>? y6)
// (30,10): warning CS8619: Nullability of reference types in value of type 'A<object>' doesn't match target type 'B<object?>?'.
// (x5 ?? y5)/*T:B<object?>?*/.Value.F.ToString();
Diagnostic(ErrorCode.WRN_NullabilityMismatchInAssignment, "x5").WithArguments("A<object>", "B<object?>?").WithLocation(30, 10),
// (30,10): warning CS8629: Nullable value type may be null.
// (x5 ?? y5)/*T:B<object?>?*/.Value.F.ToString();
Diagnostic(ErrorCode.WRN_NullableValueTypeMayBeNull, "x5 ?? y5").WithLocation(30, 10),
Copy link
Member

@333fred 333fred Dec 3, 2018

Choose a reason for hiding this comment

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

This feels like it's the wrong location. The chance for the null reference happens on the invocation of .Value, right? Unless this is reporting on the implicit conversion, in which case I feel like this isn't descriptive enough, and I would still have expected a warning on .Value. #Closed

Copy link
Member

Choose a reason for hiding this comment

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

This still feels wrong.


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

@@ -35026,6 +35042,12 @@ static void F(A<object>? x, A<object?>? y)
}";
var comp = CreateCompilation(new[] { source }, options: WithNonNullTypesTrue());
comp.VerifyDiagnostics(
// (14,24): warning CS8629: Nullable value type may be null.
// B<object>? z = x;
Diagnostic(ErrorCode.WRN_NullableValueTypeMayBeNull, "x").WithLocation(14, 24),
Copy link
Member

@333fred 333fred Dec 3, 2018

Choose a reason for hiding this comment

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

Yeah, I think this example is more illustrative of wanting to have a clearer error message here. As a user, my first thought is "The nullabilities here don't differ, why are you warning me?" #Resolved

Copy link
Member

Choose a reason for hiding this comment

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

Actually, by my reading of the spec, this is an invalid warning.

Given a user-defined conversion operator that converts from a non-nullable value type S to a non-nullable value type T, a lifted conversion operator exists that converts from S? to T?.
https://github.com/dotnet/csharplang/blob/54df8973faabe32335fa087f9d32fd959366ff83/spec/conversions.md#lifted-conversion-operators


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

Copy link
Member Author

Choose a reason for hiding this comment

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

Good catch, thanks.


In reply to: 238445282 [](ancestors = 238445282,238443507)

_ = (T)x;
x = y;
_ = (T)x; // 2
_ = (T)x;
Copy link
Member

@333fred 333fred Dec 3, 2018

Choose a reason for hiding this comment

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

Perhaps add a dereference of y after this? #Closed

}

[Fact]
public void NullableT_12()
Copy link
Member

@333fred 333fred Dec 3, 2018

Choose a reason for hiding this comment

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

Is this substantially different from NullableT_06, other than also having the negative case? If not, consider consolidating. #Closed

{
if (t1.HasValue)
{
if (t1.HasValue) _ = t1.Value;
Copy link
Member

@333fred 333fred Dec 3, 2018

Choose a reason for hiding this comment

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

Shouldn't we have a warning here for something along the lines of "this will never be false"? #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.

Logged #31516.


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

{
static void F1(A? a)
{
B? b = a; // 1
Copy link
Member

@333fred 333fred Dec 3, 2018

Choose a reason for hiding this comment

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

Again, there should not be a arning here. Top-level nullabilities match. #Closed

@333fred
Copy link
Member

333fred commented Dec 3, 2018

Done review pass (commit 3) #Resolved

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Dec 4, 2018

        builder.Add(getId(ErrorCode.WRN_ConvertingNullableToNonNullable));

Should WRN_NullableValueTypeMayBeNull be added to this list? #Closed


Refers to: src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs:23 in 5f14743. [](commit_id = 5f14743255e09c9143be2d7e10dd2eb49e25610a, deletion_comment = False)

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Dec 4, 2018

                return TypeSymbolWithAnnotations.Create(resolvedType, customModifiers: customModifiers);

Possibly dropping "NotNullable" annotation? Perhaps annotations should be passed explicitly, or an assert should be added to verify that state is not going to be lost here. #Closed


Refers to: src/Compilers/CSharp/Portable/Symbols/TypeSymbolWithAnnotations.cs:1203 in 5f14743. [](commit_id = 5f14743255e09c9143be2d7e10dd2eb49e25610a, deletion_comment = False)

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Dec 4, 2018

                return TypeSymbolWithAnnotations.Create(typeSymbol, customModifiers: customModifiers);

Here is possibly another place where state might be lost. #Closed


Refers to: src/Compilers/CSharp/Portable/Symbols/TypeSymbolWithAnnotations.cs:1213 in 5f14743. [](commit_id = 5f14743255e09c9143be2d7e10dd2eb49e25610a, deletion_comment = False)

// int?, T? where T : struct (add annotation)
nullableAnnotation = NullableAnnotation.Annotated;
case NullableAnnotation.Unknown:
case NullableAnnotation.NotAnnotated:
Copy link
Contributor

@AlekseyTs AlekseyTs Dec 4, 2018

Choose a reason for hiding this comment

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

case NullableAnnotation.NotAnnotated: [](start = 16, length = 37)

Would it make sense to add an assert to TypeSymbolWithAnnotations's constructor that Unknown and NotAnnotated are never combined with a Nullable<T>? #Closed

@@ -380,7 +380,8 @@ protected override bool TryGetReceiverAndMember(BoundExpression expr, out BoundE
// Remove this and populate struct members lazily to match classes.
private Symbol GetBackingFieldIfStructProperty(Symbol symbol)
{
if (symbol.Kind == SymbolKind.Property)
if (symbol.Kind == SymbolKind.Property &&
symbol.ContainingType.OriginalDefinition.SpecialType != SpecialType.System_Nullable_T)
Copy link
Contributor

@AlekseyTs AlekseyTs Dec 4, 2018

Choose a reason for hiding this comment

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

.OriginalDefinition.SpecialType != SpecialType.System_Nullable_T) [](start = 37, length = 65)

We have a IsNullableType helper. #Closed

{
// Explicit conversion of Nullable<T> to T is equivalent to Nullable<T>.Value.
int containingSlot = MakeSlot(operand);
return containingSlot < 0 ? -1 : GetNullableOfTValueSlot(operand.Type, containingSlot);
Copy link
Contributor

@AlekseyTs AlekseyTs Dec 4, 2018

Choose a reason for hiding this comment

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

return containingSlot < 0 ? -1 : GetNullableOfTValueSlot(operand.Type, containingSlot); [](start = 27, length = 88)

I would like to better understand why do we need to be able to get a slots for conversions. Could you provide some details? Offline would be fine. #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.

As we discussed offline, in the following, need to track slots for a, a.Value, a.Value.B and a.Value.B.Value, and when evaluating ((A)a).B, we need to recognize the nullability of (A)a (not nullable) and the slot (a.Value).

struct A { B? B; }
struct B { }
class Program
{
    static void F(A? a)
    {
        if (a?.B != null)
            _ = ((A)a).B.Value;
    }
}

See NullableT_19.


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

@@ -814,6 +829,11 @@ private void InheritNullableStateOfTrackableType(int targetSlot, int valueSlot,
}
}

private TypeSymbol GetSlotType(int slot)
{
return variableBySlot[slot].Symbol.GetTypeOrReturnType().TypeSymbol;
Copy link
Contributor

@AlekseyTs AlekseyTs Dec 4, 2018

Choose a reason for hiding this comment

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

GetTypeOrReturnType [](start = 47, length = 19)

It looks like we also have VariableType helper in the base class. Would it be more appropriate to use it here? #Closed

}

[Fact]
public void NullableT_IndexAndRange()
Copy link
Member

@333fred 333fred Dec 4, 2018

Choose a reason for hiding this comment

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

Are we expecting to get warnings here, or is this the actual final behavior? If the former, consider logging a bug. #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.

There shouldn't be any warnings here. (There were warnings in an earlier iteration of this change.)


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

Copy link
Member

@333fred 333fred left a comment

Choose a reason for hiding this comment

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

LGTM (commit 8)

{
// Explicit conversion of Nullable<T> to T is equivalent to Nullable<T>.Value.
int containingSlot = MakeSlot(operand);
return containingSlot < 0 ? -1 : GetNullableOfTValueSlot(operand.Type, containingSlot);
Copy link
Contributor

@AlekseyTs AlekseyTs Dec 4, 2018

Choose a reason for hiding this comment

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

return containingSlot < 0 ? -1 : GetNullableOfTValueSlot(operand.Type, containingSlot); [](start = 28, length = 87)

Can we get here for a conversion from Nullable<Type1> to Type2? #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.

Converting from Nullable<T1> to T2 would be either a Boxing conversion or an ExplicitUserDefined conversion.


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

Copy link
Contributor

Choose a reason for hiding this comment

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

Converting from Nullable to T2 would be either a Boxing conversion or an ExplicitUserDefined conversion.

Are you sure? What about int? -> long and the like?


In reply to: 239155510 [](ancestors = 239155510,238852151)

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Dec 4, 2018

                    // getOperandSlots will only return slots for locations that are reference types.

Is this comment still accurate with respect to nullable value types? #Closed


Refers to: src/Compilers/CSharp/Portable/FlowAnalysis/NullableWalker.cs:1549 in 5de5cae. [](commit_id = 5de5caef98a5df5dcce9aedc904cf0e83a6b8107, deletion_comment = False)

bool isLiftedConversion =
operandType.IsNullableType() &&
!parameterType.IsNullableType() &&
parameterType.Equals(underlyingOperandType = operandType.GetNullableUnderlyingType(), TypeCompareKind.AllIgnoreOptions);
Copy link
Contributor

@AlekseyTs AlekseyTs Dec 4, 2018

Choose a reason for hiding this comment

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

parameterType.Equals(underlyingOperandType = operandType.GetNullableUnderlyingType(), TypeCompareKind.AllIgnoreOptions) [](start = 28, length = 119)

It feels like this condition is too strict. For example, it is valid for a lifted conversion to have long as the parameter type and int? as the operand type. #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.

The operand -> conversion "from" type conversion above should represent the conversion from int? to long?.


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

Copy link
Contributor

Choose a reason for hiding this comment

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

Do we have a test for a scenario like this? Also, the fact that we are "redefining" what operandType represents throughout this sequence, makes it very hard to reason about the code. Consider introducing locals with specific names.


In reply to: 239170622 [](ancestors = 239170622,238872741)

Copy link
Member Author

Choose a reason for hiding this comment

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

Added tests.


In reply to: 239174936 [](ancestors = 239174936,239170622,238872741)

if (isLiftedConversion)
{
operandType = TypeSymbolWithAnnotations.Create(
operandType.IsValueType ? compilation.GetSpecialType(SpecialType.System_Nullable_T).Construct(ImmutableArray.Create(operandType)) : operandType.TypeSymbol,
Copy link
Contributor

@AlekseyTs AlekseyTs Dec 4, 2018

Choose a reason for hiding this comment

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

operandType.IsValueType ? compilation.GetSpecialType(SpecialType.System_Nullable_T).Construct(ImmutableArray.Create(operandType)) : operandType.TypeSymbol, [](start = 32, length = 155)

Unless I am misinterpreting something, I am not sure why do we need to wrap parameterType to nullable. What observable effect would that have on the analysis? #Closed

{
operandType = TypeSymbolWithAnnotations.Create(
operandType.IsValueType ? compilation.GetSpecialType(SpecialType.System_Nullable_T).Construct(ImmutableArray.Create(operandType)) : operandType.TypeSymbol,
operandAnnotation.IsAnyNullable() ? NullableAnnotation.Nullable : NullableAnnotation.NotNullable);
Copy link
Contributor

@AlekseyTs AlekseyTs Dec 4, 2018

Choose a reason for hiding this comment

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

operandAnnotation.IsAnyNullable() ? NullableAnnotation.Nullable : NullableAnnotation.NotNullable); [](start = 32, length = 98)

It is not clear why would operandAnnotation matter, the lifted conversion method is always invoked with unwrapped (not nullable) value. #Closed

{
operandType = TypeSymbolWithAnnotations.Create(
operandType.IsValueType ? compilation.GetSpecialType(SpecialType.System_Nullable_T).Construct(ImmutableArray.Create(operandType)) : operandType.TypeSymbol,
operandAnnotation.IsAnyNullable() ? NullableAnnotation.Nullable : NullableAnnotation.NotNullable);
Copy link
Contributor

@AlekseyTs AlekseyTs Dec 4, 2018

Choose a reason for hiding this comment

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

operandAnnotation.IsAnyNullable() ? NullableAnnotation.Nullable : NullableAnnotation.NotNullable [](start = 32, length = 96)

It feels like operandAnnotation shouldn't matter if methodOpt.ReturnType is nullable value type, or a reference type. #Closed

@@ -3377,9 +3427,7 @@ private bool HasTopLevelNullabilityConversion(TypeSymbolWithAnnotations source,
case ConversionKind.Boxing:
if (!operandType.IsNull && operandType.IsValueType)
{
// https://github.com/dotnet/roslyn/issues/29959 Should we worry about a pathological case of boxing nullable value known to be not null?
// For example, new int?(0)
resultAnnotation = operandType.IsNullableType() ? NullableAnnotation.Nullable : NullableAnnotation.NotNullable;
Copy link
Contributor

@AlekseyTs AlekseyTs Dec 4, 2018

Choose a reason for hiding this comment

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

operandType.IsNullableType() [](start = 43, length = 28)

I think we should keep the IsNullableType check. Malformed annotations in metadata can have nullable annotation of a non-nullable value type. #Closed

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Dec 4, 2018

            case ConversionKind.Unboxing:

It feels like we should have a special handling for unboxing into a nullable value type. #Closed


Refers to: src/Compilers/CSharp/Portable/FlowAnalysis/NullableWalker.cs:3412 in 5de5cae. [](commit_id = 5de5caef98a5df5dcce9aedc904cf0e83a6b8107, deletion_comment = False)

@cston cston merged commit 1f5f718 into dotnet:master Dec 11, 2018
@cston cston deleted the NVT branch December 11, 2018 01:08
@cston
Copy link
Member Author

cston commented Dec 11, 2018

Thanks for the review feedback!

@cston
Copy link
Member Author

cston commented Dec 11, 2018

cc @dotnet/roslyn-compiler

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