Browse Source

Classify nullable override disambiguators by reference type

A class-type constraint such as Stream or Delegate sets no
ReferenceTypeConstraint flag, so keying the disambiguator off that flag gave
those overrides the default constraint, which is CS8822, and the output still
did not recompile. The restated disambiguator leaves no metadata trace of its
own, so the choice has to follow from whether the inherited constraints make
the type parameter a reference type, a value type, or neither.

Matching the annotated type parameters by identity rather than by owner kind
and index also keeps a specialized signature from contributing a foreign type
parameter that happens to share an index.

Assisted-by: Copilot:claude-opus-5:GitHub Copilot CLI
pull/3910/head
Sebastien Lebreton 2 months ago
parent
commit
0e600382b0
  1. 121
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/Issue3909.cs
  2. 44
      ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs

121
ICSharpCode.Decompiler.Tests/TestCases/Pretty/Issue3909.cs

@ -1,3 +1,6 @@
using System;
using System.Collections.Generic;
#nullable enable #nullable enable
namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
{ {
@ -101,5 +104,123 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
return value; return value;
} }
} }
public class Node
{
}
public abstract class ConstrainedBase
{
public virtual T? ClassType<T>(T? value) where T : Node
{
return value;
}
public virtual T? DelegateType<T>(T? value) where T : Delegate
{
return value;
}
public virtual T? EnumType<T>(T? value) where T : Enum
{
return value;
}
public virtual T? InterfaceType<T>(T? value) where T : IDisposable
{
return value;
}
public virtual List<T?> NestedGeneric<T>(List<T?> values)
{
return values;
}
public virtual TItem? PartiallyAnnotated<TItem, TOther>(TItem? value, TOther other)
{
return value;
}
}
public sealed class ConstrainedDerived : ConstrainedBase
{
public override T? ClassType<T>(T? value) where T : class
{
return value;
}
public override T? DelegateType<T>(T? value) where T : class
{
return value;
}
public override T? EnumType<T>(T? value) where T : default
{
return value;
}
public override T? InterfaceType<T>(T? value) where T : default
{
return value;
}
public override List<T?> NestedGeneric<T>(List<T?> values) where T : default
{
return values;
}
public override TItem? PartiallyAnnotated<TItem, TOther>(TItem? value, TOther other) where TItem : default
{
return value;
}
}
public class ClassTypeChainBase<TOuter> where TOuter : Node
{
public virtual T? Chained<T>(T? value) where T : TOuter
{
return value;
}
}
public sealed class ClassTypeChainDerived<TOuter> : ClassTypeChainBase<TOuter> where TOuter : Node
{
public override T? Chained<T>(T? value) where T : class
{
return value;
}
}
public class ReferenceTypeChainBase<TOuter> where TOuter : class
{
public virtual T? Chained<T>(T? value) where T : TOuter
{
return value;
}
}
public sealed class ReferenceTypeChainDerived<TOuter> : ReferenceTypeChainBase<TOuter> where TOuter : class
{
public override T? Chained<T>(T? value) where T : default
{
return value;
}
}
public class ContainerBase<TOuter> where TOuter : class
{
public virtual TOuter? Pick<TItem>(TItem item)
{
return null;
}
}
public sealed class ContainerDerived<TOuter> : ContainerBase<TOuter> where TOuter : class
{
public override TOuter? Pick<TItem>(TItem item)
{
return null;
}
}
} }
} }

44
ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs

@ -2613,49 +2613,57 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
return c; return c;
} }
// The compiler records the inherited class/struct constraint flags on an override or // A disambiguator is required only where the type parameter itself carries a nullable
// explicit interface implementation in metadata, even though the source does not restate // annotation ('T?') in the signature: without it the compiler reads 'T?' as Nullable<T>.
// them, so the disambiguator can be derived from the method's own type parameters without // Which one is legal follows from whether the inherited constraints make the type
// resolving the base member. A disambiguator is required only where the type parameter // parameter a reference type, a value type, or neither:
// itself carries a nullable annotation ('T?') in the signature: without it the compiler // - a value type uses Nullable<T> rather than a nullable annotation, so it neither
// reads 'T?' as Nullable<T>. A struct-constrained parameter uses Nullable<T> rather than a // needs nor permits a disambiguator;
// nullable annotation, so it neither needs nor permits one. // - a reference type requires 'class';
// - anything else requires 'default'.
// The inherited constraints are re-emitted on the override's own type parameters in
// metadata, so this classification does not depend on resolving the base member. The
// restated disambiguator itself is not, hence it must be derived rather than read back.
void AddNullabilityDisambiguatingConstraints(MethodDeclaration decl, IMethod method) void AddNullabilityDisambiguatingConstraints(MethodDeclaration decl, IMethod method)
{ {
if (method.TypeParameters.Count == 0) if (method.TypeParameters.Count == 0)
return; return;
NullableTypeParameterCollector collector = new(); NullableTypeParameterCollector collector = new(method.TypeParameters);
method.ReturnType.AcceptVisitor(collector); method.ReturnType.AcceptVisitor(collector);
foreach (IParameter p in method.Parameters) foreach (IParameter p in method.Parameters)
p.Type.AcceptVisitor(collector); p.Type.AcceptVisitor(collector);
if (collector.MethodTypeParameterIndices.Count == 0) if (collector.NullableTypeParameters.Count == 0)
return; return;
foreach (ITypeParameter tp in method.TypeParameters) foreach (ITypeParameter tp in method.TypeParameters)
{ {
if (!collector.MethodTypeParameterIndices.Contains(tp.Index) || tp.HasValueTypeConstraint) if (!collector.NullableTypeParameters.Contains(tp))
continue;
bool? isReferenceType = tp.IsReferenceType;
if (isReferenceType == false)
continue; continue;
Constraint c = new(); Constraint c = new();
c.TypeParameter = MakeSimpleType(tp.Name); c.TypeParameter = MakeSimpleType(tp.Name);
// C# accepts only plain 'class' here, never 'class?'; the constraint's own // C# accepts only plain 'class' here, never 'class?'; the constraint's own
// nullability is inherited from the base member regardless. // nullability is inherited from the base member regardless.
c.BaseTypes.Add(new PrimitiveType(tp.HasReferenceTypeConstraint ? "class" : "default")); c.BaseTypes.Add(new PrimitiveType(isReferenceType == true ? "class" : "default"));
decl.Constraints.Add(c); decl.Constraints.Add(c);
} }
} }
// Collects the indices of a method's own type parameters that appear with a nullable // Collects the type parameters of one method that appear with a nullable annotation ('T?')
// annotation ('T?') anywhere in a visited type, including nested positions such as // anywhere in a visited type, including nested positions such as List<T?> or T?[]. Type
// List<T?> or T?[]. // parameters of any other owner are ignored: a specialized signature can substitute a
sealed class NullableTypeParameterCollector : TypeVisitor // foreign type parameter that happens to share an index with one of this method's own.
sealed class NullableTypeParameterCollector(IReadOnlyList<ITypeParameter> typeParameters) : TypeVisitor
{ {
public readonly HashSet<int> MethodTypeParameterIndices = []; public readonly HashSet<ITypeParameter> NullableTypeParameters = [];
public override IType VisitNullabilityAnnotatedType(NullabilityAnnotatedType type) public override IType VisitNullabilityAnnotatedType(NullabilityAnnotatedType type)
{ {
if (type is NullabilityAnnotatedTypeParameter { Nullability: Nullability.Nullable } natp if (type is NullabilityAnnotatedTypeParameter { Nullability: Nullability.Nullable } natp
&& natp.OriginalTypeParameter.OwnerType == SymbolKind.Method) && typeParameters.Contains(natp.OriginalTypeParameter))
{ {
MethodTypeParameterIndices.Add(natp.OriginalTypeParameter.Index); NullableTypeParameters.Add(natp.OriginalTypeParameter);
} }
return base.VisitNullabilityAnnotatedType(type); return base.VisitNullabilityAnnotatedType(type);
} }

Loading…
Cancel
Save