-
Notifications
You must be signed in to change notification settings - Fork 4.1k
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
Conversation
@@ -835,9 +835,10 @@ private void CheckNewModifier(Symbol symbol, bool isNew, DiagnosticBag diagnosti | |||
for (int i = 0; i < overridingParameters.Length; i++) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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), |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@@ -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), |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 typeT
, a lifted conversion operator exists that converts fromS?
toT?
.
https://github.com/dotnet/csharplang/blob/54df8973faabe32335fa087f9d32fd959366ff83/spec/conversions.md#lifted-conversion-operators
In reply to: 238443507 [](ancestors = 238443507)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_ = (T)x; | ||
x = y; | ||
_ = (T)x; // 2 | ||
_ = (T)x; |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
{ | ||
static void F1(A? a) | ||
{ | ||
B? b = a; // 1 |
There was a problem hiding this comment.
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
Done review pass (commit 3) #Resolved |
Should Refers to: src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs:23 in 5f14743. [](commit_id = 5f14743255e09c9143be2d7e10dd2eb49e25610a, deletion_comment = False) |
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) |
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: |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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)
There was a problem hiding this 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); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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)
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); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (isLiftedConversion) | ||
{ | ||
operandType = TypeSymbolWithAnnotations.Create( | ||
operandType.IsValueType ? compilation.GetSpecialType(SpecialType.System_Nullable_T).Construct(ImmutableArray.Create(operandType)) : operandType.TypeSymbol, |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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
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) |
Thanks for the review feedback! |
cc @dotnet/roslyn-compiler |
No description provided.