diff --git a/ICSharpCode.Decompiler.Tests/Semantics/TypeInferenceTests.cs b/ICSharpCode.Decompiler.Tests/Semantics/TypeInferenceTests.cs index e70ab6579..8ff5c4dde 100644 --- a/ICSharpCode.Decompiler.Tests/Semantics/TypeInferenceTests.cs +++ b/ICSharpCode.Decompiler.Tests/Semantics/TypeInferenceTests.cs @@ -1257,14 +1257,26 @@ namespace ICSharpCode.Decompiler.Tests.Semantics Assert.That(success); } + [Test] + public void BestCommonTypeObjectAndNullableString() + { + Assert.That( + ti.GetBestCommonType([ + new ResolveResult(compilation.FindType(KnownTypeCode.Object).ChangeNullability(Nullability.NotNullable)), + new ResolveResult(compilation.FindType(KnownTypeCode.String).ChangeNullability(Nullability.Nullable)) + ], out bool success), + Is.EqualTo(compilation.FindType(KnownTypeCode.Object).ChangeNullability(Nullability.Nullable))); + Assert.That(success); + } + [Test] public void BestCommonTypeObjectAndNullableObject() { Assert.That( - ti.GetBestCommonType(new[] { + ti.GetBestCommonType([ new ResolveResult(compilation.FindType(KnownTypeCode.Object).ChangeNullability(Nullability.NotNullable)), new ResolveResult(compilation.FindType(KnownTypeCode.Object).ChangeNullability(Nullability.Nullable)) - }, out bool success), + ], out bool success), Is.EqualTo(compilation.FindType(KnownTypeCode.Object).ChangeNullability(Nullability.Nullable))); Assert.That(success); } diff --git a/ICSharpCode.Decompiler/CSharp/Resolver/TypeInference.cs b/ICSharpCode.Decompiler/CSharp/Resolver/TypeInference.cs index 959982ec9..d668795bd 100644 --- a/ICSharpCode.Decompiler/CSharp/Resolver/TypeInference.cs +++ b/ICSharpCode.Decompiler/CSharp/Resolver/TypeInference.cs @@ -1222,16 +1222,32 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver // Deduplicate types. This also merges types that differ only in tuple // element names and/or object/dynamic. + // The specification doesn't mention this step, but Roslyn does it, + // and it's crucial for tuple element names and nested nullabilities in otherwise + // equivalent types. + Nullability? topLevelNullability = null; var candidateMergeDict = new Dictionary(); - void AddCandidates(IEnumerable candidates, VarianceModifier variance) + void AddCandidates(IReadOnlyCollection bounds, VarianceModifier variance) { - foreach (var candidate in candidates) + // This helper function works like Roslyn's MethodTypeInference.AddAllCandidates(). + // It deduplicates similar types and merges them into a single candidate type, + // handling differences in nullability, tuple element names, and object/dynamic, + // but only if the type is otherwise completely identical. + foreach (var bound in bounds) { - var key = candidate.AcceptVisitor(NormalizeTypeVisitor.KeyForTypeMerging); + if (topLevelNullability.HasValue) + { + topLevelNullability = MergeNullability(topLevelNullability.Value, bound.Nullability, variance); + } + else + { + topLevelNullability = bound.Nullability; + } + var key = bound.AcceptVisitor(NormalizeTypeVisitor.KeyForTypeMerging); if (candidateMergeDict.TryGetValue(key, out var existing)) { - var merged = MergeSimilarTypes(existing, candidate, variance); - Log.WriteLine(" Merged similar types " + existing + " and " + candidate + " into " + merged); + var merged = MergeSimilarTypes(existing, bound, variance); + Log.WriteLine(" Merged similar types " + existing + " and " + bound + " into " + merged); if (merged != null) { candidateMergeDict[key] = merged; @@ -1244,7 +1260,7 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver } else { - candidateMergeDict.Add(key, candidate); + candidateMergeDict.Add(key, bound); } } } @@ -1267,6 +1283,15 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver c => candidateTypes.All(o => conversions.ImplicitConversion(o, c).IsValid) ).ToList(); + // Apply the merged top-level nullability: + Debug.Assert(topLevelNullability.HasValue); + // (Roslyn has a different approach in MergeOrRemoveCandidates, but to me that just looked + // like an overly complicated way of achieving the same thing.) + for (int i = 0; i < candidateTypes.Count; i++) + { + candidateTypes[i] = candidateTypes[i].ChangeNullability(topLevelNullability.Value); + } + // If the specified algorithm produces a single candidate, we return // that candidate. // We also return the whole candidate list if we're not using the improved