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

Support nullable annotations on unconstrained type parameters #45993

Merged
merged 28 commits into from
Jul 23, 2020

Conversation

cston
Copy link
Member

@cston cston commented Jul 15, 2020

Support ? annotations on unconstrained type parameter references:

  • ? annotation is allowed for type parameters that are not constrained to reference types or value types
  • ? annotation is an error when used on unconstrained type parameter with /langversion:8

Support where T : default constraint:

  • default constraint is allowed for method type parameters in overridden or explicitly implemented methods to disambiguate between overloads defined in the base or interface
  • default constraint is an error with /langversion:8
  • default constraint is an error when used outside of method overrides or explicit implementations
  • default constraint is an error when the type parameter is constrained

Fixes #29146

@cston cston force-pushed the default-constraint branch 4 times, most recently from c22cd86 to eb3bf46 Compare July 16, 2020 00:22
@cston cston force-pushed the default-constraint branch from 4b2e7a2 to 378b787 Compare July 16, 2020 05:41
@cston cston marked this pull request as ready for review July 16, 2020 05:43
@cston cston requested a review from a team as a code owner July 16, 2020 05:43
@@ -53,7 +53,7 @@ public static bool CanBeConst(this TypeSymbol typeSymbol)
/// T where T : IComparable => true
/// T where T : IComparable? => true
/// </summary>
public static bool IsTypeParameterDisallowingAnnotation(this TypeSymbol type)
public static bool IsTypeParameterDisallowingAnnotation(this TypeSymbol type) // The method name is misleading since annotations are allowed always.
Copy link
Member

@jaredpar jaredpar Jul 16, 2020

Choose a reason for hiding this comment

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

Think we should move this comment to a <remarks> section. #Resolved

/// </summary>
public void TryForceResolveAsNullableValueType()
public void TryForceResolve(bool asValueTypeNotReferenceType)
Copy link
Member

@jaredpar jaredpar Jul 16, 2020

Choose a reason for hiding this comment

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

Consider asValueType. #Resolved

? NullableAnnotation.NotAnnotated : NullableAnnotation.Annotated;
if (Type is null)
{
return TypeWithAnnotations.Create(Type, NullableAnnotation.Annotated);
Copy link
Member

@jaredpar jaredpar Jul 16, 2020

Choose a reason for hiding this comment

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

Suggested change
return TypeWithAnnotations.Create(Type, NullableAnnotation.Annotated);
return TypeWithAnnotations.Create(null, NullableAnnotation.Annotated);

Why do we unconditionally use NullableAnnotation.Annotated here for null types? #Resolved

var formatWithNullableModifier = formatWithoutModifiers.AddMiscellaneousOptions(SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier);
var formatWithBothModifiers = formatWithNullableModifier.AddMiscellaneousOptions(SymbolDisplayMiscellaneousOptions.IncludeNotNullableReferenceTypeModifier);

verify("C.F0", "T F0<T>()", "T F0<T>()", "T F0<T>()");
Copy link
Member

@jaredpar jaredpar Jul 16, 2020

Choose a reason for hiding this comment

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

Why is the final one not T! F0<T>()? #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.

The behavior of SymbolDisplayMiscellaneousOptions.IncludeNotNullableReferenceTypeModifier is unchanged. The type could be reported as T! here, and in T F8<T>() where T : notnull, independent of this PR.


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


if (Type is null)
{
return NullableAnnotation.NotAnnotated;
Copy link
Member

@jaredpar jaredpar Jul 16, 2020

Choose a reason for hiding this comment

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

Why do we prefer NotAnnotated here over Oblivious? #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.

I've removed the explicit Type is null case so the behavior should match the current behavior which returns this.NullableAnnotation. That said, I suspect NullableAnnotation is typically NotAnnotated when Type is null since that is the default value for the struct.


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

Comment on lines 127826 to 127827
[InlineData(false)]
[InlineData(true)]
Copy link
Member

@jaredpar jaredpar Jul 16, 2020

Choose a reason for hiding this comment

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

Suggested change
[InlineData(false)]
[InlineData(true)]
[CombinatorialData]
``` #Resolved

@cston cston requested a review from a team July 17, 2020 16:02
@@ -5603,7 +5603,7 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ
<value>Explicit application of 'System.Runtime.CompilerServices.NullableAttribute' is not allowed.</value>
</data>
<data name="ERR_NullableUnconstrainedTypeParameter" xml:space="preserve">
<value>A nullable type parameter must be known to be a value type or non-nullable reference type. Consider adding a 'class', 'struct', or type constraint.</value>
<value>A nullable type parameter must be known to be a value type or non-nullable reference type. Please use language version '{0}' or greater, or consider adding a 'class', 'struct', or type constraint.</value>
Copy link
Contributor

@AlekseyTs AlekseyTs Jul 17, 2020

Choose a reason for hiding this comment

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

Please use language version '{0}' [](start = 102, length = 34)

I think we should simply use regular language version message with specific feature name, etc. This way IDE will be able to suggest the change language version fix. #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 message has helpful suggestions for using T? in C#8 which the general language version message would not. We can update the IDE to recognize this ErrorCode if necessary.


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

Copy link
Contributor

Choose a reason for hiding this comment

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

We can update the IDE to recognize this ErrorCode if necessary.

Let's do that then. I think we already have a mechanism to communicate to IDE that it should offer an upgrade to specific language version. CC @jcouv


In reply to: 456613552 [](ancestors = 456613552,456600909)

@@ -5731,6 +5731,9 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ
<data name="IDS_FeatureNullPointerConstantPattern" xml:space="preserve">
<value>null pointer constant pattern</value>
</data>
<data name="IDS_FeatureDefaultTypeParameterConstraint" xml:space="preserve">
<value>default type parameter constraints</value>
Copy link
Contributor

@AlekseyTs AlekseyTs Jul 17, 2020

Choose a reason for hiding this comment

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

default type parameter constraints [](start = 11, length = 34)

It feels like the name of the feature isn't very clear, especially that default is used in not very common cases. For example, a user typed T? and compiler reports that "default type parameter constraints" aren't supported by the language version. What constraints the message is talking about? This isn't a constraint. #Closed

Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps we need another feature name for T?


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

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 message is only reported for uses of where T : default. For uses of T?, we continue to report ErrorCode.ERR_NullableUnconstrainedTypeParameter.


In reply to: 456603253 [](ancestors = 456603253,456602711)

@@ -1908,6 +1909,9 @@ private TypeParameterConstraintSyntax ParseTypeParameterConstraint()
questionToken = this.TryEatToken(SyntaxKind.QuestionToken);

return _syntaxFactory.ClassOrStructConstraint(SyntaxKind.ClassConstraint, classToken, questionToken);
case SyntaxKind.DefaultKeyword:
var defaultToken = this.EatToken();
return _syntaxFactory.DefaultConstraint(defaultToken);
Copy link
Contributor

@AlekseyTs AlekseyTs Jul 17, 2020

Choose a reason for hiding this comment

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

_syntaxFactory.DefaultConstraint(defaultToken) [](start = 27, length = 46)

Should we check language version here? #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 version check is in BindTypeParameterConstraints().


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

Copy link
Contributor

Choose a reason for hiding this comment

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

The version check is in BindTypeParameterConstraints().

I realized that. The question is why? Usually, for simple cases like that we perform language version checks in the parser.


In reply to: 456693574 [](ancestors = 456693574,456659327)

@@ -334,6 +335,7 @@ internal static LanguageVersion RequiredVersion(this MessageID feature)
case MessageID.IDS_FeatureRecords:
case MessageID.IDS_FeatureStaticAnonymousFunction: // syntax check
case MessageID.IDS_FeatureModuleInitializers: // semantic check on method attribute
case MessageID.IDS_FeatureDefaultTypeParameterConstraint:
Copy link
Contributor

@AlekseyTs AlekseyTs Jul 17, 2020

Choose a reason for hiding this comment

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

case MessageID.IDS_FeatureDefaultTypeParameterConstraint: [](start = 16, length = 57)

Consider adding a comment if this is not a parsing time check #Closed

{
if (type.Type.IsTypeParameterDisallowingAnnotation())
{
var requiredVersion = MessageID.IDS_FeatureDefaultTypeParameterConstraint.RequiredVersion();
Copy link
Contributor

@AlekseyTs AlekseyTs Jul 17, 2020

Choose a reason for hiding this comment

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

IDS_FeatureDefaultTypeParameterConstraint [](start = 48, length = 41)

This is somewhat confusing because default constraint isn't involved. #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.

Added a comment.


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

private static bool IsPossiblyNullableTypeTypeParameter(in TypeWithAnnotations typeWithAnnotations)
{
var type = typeWithAnnotations.Type;
return type is object &&
Copy link
Contributor

@AlekseyTs AlekseyTs Jul 17, 2020

Choose a reason for hiding this comment

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

return type is object && [](start = 12, length = 24)

It looks like we dropped IsNotAnnotated check. Is this intentional? Could you please elaborate? #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.

It looks like we dropped IsNotAnnotated check. Is this intentional?

Yes. Callers above are explicitly checking NullableAnnotation, and requiring NullableAnnotation == NullableAnnotation.NotAnnotated here would mean T and T? would be considered identical in HasTopLevelNullabilityIdentityConversion(). For instance, that would allow assigning IEnumerable<T?> to IEnumerable<T> in UnconstrainedTypeParameter_21().


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

@cston cston force-pushed the default-constraint branch from b9d17f3 to 6fff28c Compare July 23, 2020 06:18
@cston cston force-pushed the default-constraint branch 2 times, most recently from 440cb3b to 280bdf4 Compare July 23, 2020 07:15
@jaredpar
Copy link
Member

The spanish failure is in UpdaterService which is flaky. Will filean issue shortly. Merging.

@jaredpar jaredpar merged commit cbb5cf1 into dotnet:release/dev16.8-preview1 Jul 23, 2020
@cston cston deleted the default-constraint branch July 23, 2020 18:09
333fred added a commit to 333fred/roslyn that referenced this pull request Jul 28, 2020
…-pointers

* upstream/master: (207 commits)
  Update argument state when parameter has not-null type (dotnet#46072)
  Fix TypeWithAnnotations.ToTypeWithState() for (untyped) null literal (dotnet#46344)
  Update README (dotnet#46136)
  Revert "Revert "Support nullable annotations on unconstrained type parameters""
  Revert "Support nullable annotations on unconstrained type parameters (dotnet#45993)"
  Fix type in publish data
  Update VSIXExpInstaller version to one available on ADO
  Update publish data for 16.8
  Update version of RichCodeNav.EnvVarDump
  A fixed initializer must be bound to its natural type (dotnet#46293)
  Update features merged into 16.7p4 (dotnet#46229)
  Async-streams: disposal should continue without jump within a finally (dotnet#46188)
  Recommend default in type constraint, but not record (dotnet#46311)
  Add use site diagnostics to IsUnmanaged (dotnet#46114)
  Add another flaky test.
  Ensure NuGet connections use TLS 1.2
  Update to Microsoft.CodeAnalysis.Testing 1.0.1-beta1.20374.2
  Skip flaky test.
  Fix build break. (dotnet#46303)
  Skip a flaky test Relates to dotnet#46304
  ...
333fred added a commit to 333fred/roslyn that referenced this pull request Jul 28, 2020
…to function-pointer-type-lookup

* upstream/features/function-pointers: (212 commits)
  Correct public API number.
  Update argument state when parameter has not-null type (dotnet#46072)
  Fix TypeWithAnnotations.ToTypeWithState() for (untyped) null literal (dotnet#46344)
  Update README (dotnet#46136)
  Revert "Revert "Support nullable annotations on unconstrained type parameters""
  Revert "Support nullable annotations on unconstrained type parameters (dotnet#45993)"
  Fix type in publish data
  Update VSIXExpInstaller version to one available on ADO
  Update publish data for 16.8
  Update version of RichCodeNav.EnvVarDump
  A fixed initializer must be bound to its natural type (dotnet#46293)
  Update features merged into 16.7p4 (dotnet#46229)
  Async-streams: disposal should continue without jump within a finally (dotnet#46188)
  Recommend default in type constraint, but not record (dotnet#46311)
  Add use site diagnostics to IsUnmanaged (dotnet#46114)
  Add another flaky test.
  Ensure NuGet connections use TLS 1.2
  Update to Microsoft.CodeAnalysis.Testing 1.0.1-beta1.20374.2
  Skip flaky test.
  Fix build break. (dotnet#46303)
  ...
@mysteryx93
Copy link

Why was T? on unconstrained types implemented as meaning ValueOrDefault instead of ValueOrNull?

For ValueOrNull, we're still back to the old method of duplicating classes with struct/class constraints.

@RikkiGibson
Copy link
Contributor

Could you please share some specific scenarios that are negatively affected by this?

@mysteryx93
Copy link

mysteryx93 commented Apr 19, 2021

The following code compiles but doesn't behave as one would expect. Previously it would fail to compile, now it just doesn't do what I'd expect it to.

public enum LogLevel { None, Debug, Info }

public interface IFormatter<T>
{
    T Parse(string? value);
    string? Format(T value);
}

public class PropertyBase<T> : IFormatter<T?>
{
    public virtual T? Parse(string? value) => default!;
    public virtual string? Format(T? value) => null;
}

public class PropertyRead<T> : PropertyBase<T>
{ }

public class PropertyReadWrite<T> : PropertyRead<T>
{ }

public class PropertyEnum<T> : PropertyRead<T>
    where T : struct, Enum
{ }

public class MyClass
{
    public void DoIt()
    {
        var f = new PropertyEnum<LogLevel>();
        var value = f.Parse(null); // non-nullable!
    }
}

The solution is to write it like this

public enum LogLevel { None, Debug, Info }

public interface IFormatter<T>
{
    T Parse(string? value);
    string? Format(T value);
}

public class PropertyBase<TNull> : IFormatter<TNull>
{
    public virtual TNull Parse(string? value) => default!;
    public virtual string? Format(TNull value) => null;
}

public class PropertyRead<T> : PropertyBase<T?>
    where T : struct
{ }

public class PropertyReadRef<T> : PropertyBase<T?>
    where T : class
{ }

public class PropertyReadWrite<T> : PropertyRead<T>
    where T : struct
{ }

public class PropertyReadWriteRef<T> : PropertyReadRef<T>
    where T : class
{ }

public class PropertyEnum<T> : PropertyRead<T>
    where T : struct, Enum
{ }

public class MyClass
{
    public void DoIt()
    {
        var f = new PropertyEnum<LogLevel>();
        var value = f.Parse(null); // nullable!
    }
}

This solution however requires a lot of code copy/paste, and inheritance references can get complicated, especially when the classes contain considerable code.

And the syntax is weird for string: PropertyReadRef<string>, which leads me to creating a shortcut class PropertyReadString

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.

Mechanism to annotate unconstrained type parameters
6 participants