-
Notifications
You must be signed in to change notification settings - Fork 4k
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
Nullable conversion of collection expressions #69852
Conversation
262af09
to
e92a30f
Compare
e92a30f
to
25a1a98
Compare
{ | ||
static MyCollection<int>? M() | ||
{ | ||
return (MyCollection<int>?)[1, 2, 3]; |
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.
can you have a test with (MyCollection<byte>?)[1, 2, 3];
#Resolved
@@ -541,6 +543,14 @@ void checkConstraintLanguageVersionAndRuntimeSupportForConversion(SyntaxNode syn | |||
Conversion conversion, | |||
BindingDiagnosticBag diagnostics) | |||
{ | |||
if (targetType.IsNullableType()) | |||
{ | |||
Debug.Assert(conversion.IsNullable); |
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 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.
Good catch. Thanks
private static bool IsSystemNullable(TypeSymbol type, [NotNullWhen(true)] out TypeSymbol? underlyingType) | ||
{ | ||
if (type is NamedTypeSymbol nt | ||
&& nt.OriginalDefinition.GetSpecialTypeSafe() == 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.
It looks like we can use the SpecialType
property directly. Consider combining all and using a pattern:
type is NamedTypeSymbol
{
OriginalDefinition.SpecialType: SpecialType.System_Nullable_T,
TypeArgumentsWithAnnotationsNoUseSiteDiagnostics: [var typeArg]
}
``` #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, given that this method is used for several existing callers that were using GetSpecialTypeSafe()
, perhaps it makes sense to keep as GetSpecialTypeSafe()
to reduce risk.
@@ -4705,10 +4790,27 @@ class Program | |||
} |
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.
Consider updating the test to execute these methods, and perhaps additional methods with values, similar to TypeParameter_01
. It will require adding a List<T> _list;
field to S<T>
.
static T Create3<T, U>(U x, U y) where T : struct, I<U> => [x, y];
static T? Create4<T, U>(U x, U y) where T : struct, I<U> => [x, y];
``` #Resolved
|
||
var verifier = CompileAndVerify(comp, expectedOutput: IncludeExpectedOutput("[1, 2, 3],"), verify: Verification.Fails); | ||
|
||
// ILVerify: |
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.
@cston I found this surprising. Let's discuss offline #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.
Consider removing or slimming down this comment.
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.
Filed #69860 to track refreshing ILVerify bits and filing follow-up runtime repo issue if needed.
@333fred @RikkiGibson for another review. Thanks |
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.
Done review pass
var underlyingConversion = GetCollectionExpressionConversion(collectionExpression, underlyingDestination, ref useSiteInfo); | ||
if (underlyingConversion.Exists) | ||
{ | ||
return new Conversion(ConversionKind.ImplicitNullable, ImmutableArray.Create(underlyingConversion)); |
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.
I think we need to update the speclet to more correctly convey the logic here. It currently states that a collection expression conversion exists from a collection expression to T?
, where there exists a collection expression conversion from a collection expression to the underlying type T
if T?
is a nullable value type. We instead want the speclet to convey that some types of implicit conversions are allowed on top of collection expression conversions, such as implicit nullable conversions.
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.
I'll leave that up to Chuck who made the spec update
src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/ConversionsBase.cs
Outdated
Show resolved
Hide resolved
src/Compilers/CSharp/Portable/Binder/Semantics/OverloadResolution/MethodTypeInference.cs
Outdated
Show resolved
Hide resolved
@@ -233,7 +233,9 @@ static bool filterConversion(Conversion conversion) | |||
|
|||
if (source.Kind == BoundKind.UnconvertedCollectionExpression) | |||
{ | |||
Debug.Assert(conversion.IsCollectionExpression || !conversion.Exists); | |||
Debug.Assert(conversion.IsCollectionExpression | |||
|| (conversion.IsNullable && conversion.UnderlyingConversions[0].IsCollectionExpression) |
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:
|| (conversion.IsNullable && conversion.UnderlyingConversions[0].IsCollectionExpression) | |
|| (conversion is { IsNullable: true, UnderlyingConversions: [{ IsCollectionExpression: true }] }) | |
``` #ByDesign |
} | ||
else if (boundNodeForSyntacticParent is BoundConversion parentConversion) | ||
{ | ||
// There was an explicit cast on top of the collection expression. |
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.
In this case, I would not expect ConvertedType
to contain something different than the current type. ConvertedType
is explicitly documented as implicit conversions only. #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.
I didn't know that. Thanks for pointing it out!
|
||
var typeInfo = model.GetTypeInfo(value); | ||
Assert.Null(typeInfo.Type); | ||
Assert.Equal("MyCollection<System.Int32>", typeInfo.ConvertedType.ToTestDisplayString()); |
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.
Tagging @AlekseyTs to confirm this is indeed the behavior we want for ConvertedType
in cast scenario. #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.
This is not what I would expect. I would expect the type and the converted type to be null
. There is an explicit nullable conversion on top of this, with an underlying implicit collection conversion on the input to it.
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.
I see. Let's chat with Aleksey on Monday to confirm. If that's the case, then the previous behavior (absent nullable scenarios) was incorrect.
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.
Confirmed the behavior of ConvertedType
with some other typeless expression. I'll go with that.
[Fact]
public void TODO2()
{
string src = """
class Program
{
static int? M()
{
return (int?)null;
}
}
""";
var comp = CreateCompilation(src);
comp.VerifyDiagnostics();
var tree = comp.SyntaxTrees.First();
var model = comp.GetSemanticModel(tree);
var value = tree.GetRoot().DescendantNodes().OfType<CastExpressionSyntax>().Last().Expression;
var typeInfo = model.GetTypeInfo(value);
Assert.Null(typeInfo.Type);
Assert.Null(typeInfo.ConvertedType);
}
@@ -2201,7 +2201,7 @@ private static bool IsUserDefinedTrueOrFalse(BoundUnaryOperator @operator) | |||
else if (boundNodeForSyntacticParent is BoundConversion parentConversion) | |||
{ | |||
// There was an explicit cast on top of the collection expression. | |||
convertedType = parentConversion.Type; | |||
convertedType = highestBoundExpr.Type; | |||
convertedNullability = nullability; | |||
conversion = parentConversion.Conversion; |
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 is also incorrect. #Resolved
… to collection types in code
…into nullable-collections
public static bool IsSystemNullable(this TypeSymbol? type, [NotNullWhen(true)] out TypeSymbol? underlyingType) | ||
{ | ||
if (type is NamedTypeSymbol nt | ||
&& nt.OriginalDefinition.GetSpecialTypeSafe() == 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.
what's the difference between this and IsNullableType above? specifically, why GetSpecialTypeSafe() versus .SpecialType? #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.
should IsNullableType defer to IsSystemNullable so everything is consistent?
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.
why do you need to check if its' a NamedType if you're already going use the SpecialTYpe?
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.
what's the difference between this and IsNullableType above?
The difference is that we're returning the underlying type.
specifically, why GetSpecialTypeSafe() versus .SpecialType?
This is a refactoring of existing code. GetSpecialTypeSafe
is just SpecialType
plus a null check. That said, we don't need GetSpecialTypeSafe()
here since OriginalDefinition
cannot be null.
should IsNullableType defer to IsSystemNullable so everything is consistent?
I don't see a need.
why do you need to check if its' a NamedType if you're already going use the SpecialTYpe?
We can only access TypeArgumentsWithAnnotationsNoUseSiteDiagnostics
on a NamedTypeSymbol
.
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.
My basic point is that the these functions are virtually the same, except for the convenience of getting the type argument. As such, keeping them as similar as possible (naming, impl, etc.) seems like the best thing to do. Having them be subtly different just seems like a way for bugs to creep in.
Wrt the named type check. It seems better to do the special type check first, then just do a cast afterwards.
@@ -1129,6 +1129,32 @@ private Conversion ClassifyImplicitBuiltInConversionFromExpression(BoundExpressi | |||
return Conversion.NoConversion; | |||
} | |||
|
|||
#nullable enable | |||
private Conversion GetImplicitCollectionExpressionConversion(BoundUnconvertedCollectionExpression collectionExpression, TypeSymbol destination, CompoundUseSiteInfo<AssemblySymbol> useSiteInfo) |
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.
conversion = convertedCollection.CollectionTypeKind == CollectionExpressionTypeKind.None | ||
? Conversion.NoConversion | ||
: Conversion.CollectionExpression; | ||
// Explicit cast or error scenario like `object 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.
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.
Yes, this case still covers explicit casts (test NullableValueType_ExplicitCast
for instance).
The behavior for ConvertedType
and the conversion matches what we have for other typeless expressions with an explicit cast:
[Fact]
public void TODO2()
{
string src = """
partial class Program
{
static object M()
{
return (object)null;
}
}
""";
var comp = CreateCompilation(src, targetFramework: TargetFramework.Net80);
comp.VerifyDiagnostics();
var tree = comp.SyntaxTrees.First();
var model = comp.GetSemanticModel(tree);
var value = tree.GetRoot().DescendantNodes().OfType<CastExpressionSyntax>().Last().Expression;
Assert.Equal("null", value.ToFullString());
var conversion = model.GetConversion(value);
Assert.True(conversion.IsIdentity);
var typeInfo = model.GetTypeInfo(value);
Assert.Null(typeInfo.Type);
Assert.Null(typeInfo.ConvertedType);
}
@@ -146,6 +146,19 @@ public static TypeSymbol GetNullableUnderlyingType(this TypeSymbol type) | |||
return type.GetNullableUnderlyingTypeWithAnnotations().Type; | |||
} | |||
|
|||
public static bool IsSystemNullable(this TypeSymbol? type, [NotNullWhen(true)] out TypeSymbol? underlyingType) |
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 (speculationAnalyzer.ReplacementChangesSemantics()) | ||
return false; | ||
// HACK: Workaround lack of compiler information for collection expression conversions with casts. |
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 fix this in #68826? If so, let's reference the issue here instead.
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.
I'll let @CyrusNajmabadi answer that one, since this is part of his change.
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. Just a couple of nits
@@ -2851,6 +2851,91 @@ static void Main() | |||
comp.VerifyEmitDiagnostics(); | |||
} | |||
|
|||
[Fact] | |||
public void TypeInference_Nullable() |
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.
Nullable
is a bit of an ambiguous term now, let's clarify what nullable we mean :).
public void TypeInference_Nullable() | |
public void TypeInference_NullableValueType() | |
``` #Resolved |
} | ||
|
||
[Fact] | ||
public void TypeInference_Nullable_ExtensionMethod() |
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.
public void TypeInference_Nullable_ExtensionMethod() | |
public void TypeInference_NullableValueType_ExtensionMethod() | |
``` #Resolved |
src/Analyzers/CSharp/Analyzers/UseCollectionExpression/UseCollectionExpressionHelpers.cs
Show resolved
Hide resolved
…ectionExpressionHelpers.cs
Closes #69447
Relates to test plan #66418