diff --git a/ICSharpCode.NRefactory.CSharp/Resolver/CSharpConversions.cs b/ICSharpCode.NRefactory.CSharp/Resolver/CSharpConversions.cs index 22c5d2b128..9c9296acf2 100644 --- a/ICSharpCode.NRefactory.CSharp/Resolver/CSharpConversions.cs +++ b/ICSharpCode.NRefactory.CSharp/Resolver/CSharpConversions.cs @@ -779,17 +779,22 @@ namespace ICSharpCode.NRefactory.CSharp.Resolver return best; } - OperatorInfo SelectOperator(IType mostSpecificSource, IType mostSpecificTarget, IList operators) + Conversion SelectOperator(IType mostSpecificSource, IType mostSpecificTarget, IList operators, bool isExplicit) { + Func create = op => isExplicit ? Conversion.UserDefinedExplicitConversion(op.Method, op.IsLifted) : Conversion.UserDefinedImplicitConversion(op.Method, op.IsLifted); + var selected = operators.Where(op => op.SourceType.Equals(mostSpecificSource) && op.TargetType.Equals(mostSpecificTarget)).ToList(); + if (selected.Count == 0) + return null; + if (selected.Count == 1) - return selected[0]; + return create(selected[0]); int nNonLifted = selected.Count(s => !s.IsLifted); if (nNonLifted == 1) - return selected.First(s => !s.IsLifted); + return create(selected.First(s => !s.IsLifted)); - return null; // Ambiguous or none available + return Conversion.None; // Ambiguous // If there was no non-lifted operator, all of them must have been lifted; so the // "selected.Count == 1" check above should have found the unique lifted operator. } @@ -800,18 +805,27 @@ namespace ICSharpCode.NRefactory.CSharp.Resolver var operators = GetApplicableConversionOperators(fromResult, fromType, toType, false); if (operators.Count > 0) { - IType S0 = NullableType.GetUnderlyingType(fromType); - IType T0 = NullableType.GetUnderlyingType(toType); - - var mostSpecificSource = operators.Any(op => op.SourceType.Equals(S0)) ? S0 : FindMostEncompassedType(operators.Select(op => op.SourceType)); + var mostSpecificSource = operators.Any(op => op.SourceType.Equals(fromType)) ? fromType : FindMostEncompassedType(operators.Select(op => op.SourceType)); if (mostSpecificSource == null) return Conversion.None; - var mostSpecificTarget = operators.Any(op => op.TargetType.Equals(T0)) ? T0 : FindMostEncompassingType(operators.Select(op => op.TargetType)); + var mostSpecificTarget = operators.Any(op => op.TargetType.Equals(toType)) ? toType : FindMostEncompassingType(operators.Select(op => op.TargetType)); if (mostSpecificTarget == null) - return Conversion.None; + return NullableType.IsNullable(toType) ? UserDefinedImplicitConversion(fromResult, fromType, NullableType.GetUnderlyingType(toType)) : Conversion.None; - var selected = SelectOperator(mostSpecificSource, mostSpecificTarget, operators); - return selected != null ? Conversion.UserDefinedImplicitConversion(selected.Method, selected.IsLifted) : Conversion.None; + var selected = SelectOperator(mostSpecificSource, mostSpecificTarget, operators, false); + if (selected != null) { + if (selected.IsLifted && NullableType.IsNullable(toType)) { + // Prefer A -> B -> B? over A -> A? -> B? + var other = UserDefinedImplicitConversion(fromResult, fromType, NullableType.GetUnderlyingType(toType)); + if (other.IsValid) + return other; + } + return selected; + } + else if (NullableType.IsNullable(toType)) + return UserDefinedImplicitConversion(fromResult, fromType, NullableType.GetUnderlyingType(toType)); + else + return Conversion.None; } else { return Conversion.None; @@ -823,14 +837,11 @@ namespace ICSharpCode.NRefactory.CSharp.Resolver // C# 4.0 spec ยง6.4.5 User-defined implicit conversions var operators = GetApplicableConversionOperators(fromResult, fromType, toType, true); if (operators.Count > 0) { - IType S0 = NullableType.GetUnderlyingType(fromType); - IType T0 = NullableType.GetUnderlyingType(toType); - IType mostSpecificSource; - if (operators.Any(op => op.SourceType.Equals(S0))) { - mostSpecificSource = S0; + if (operators.Any(op => op.SourceType.Equals(fromType))) { + mostSpecificSource = fromType; } else { - var operatorsWithSourceEncompassingFromType = operators.Where(op => IsEncompassedBy(S0, op.SourceType) || ImplicitConstantExpressionConversion(fromResult, op.SourceType)); + var operatorsWithSourceEncompassingFromType = operators.Where(op => IsEncompassedBy(fromType, op.SourceType) || ImplicitConstantExpressionConversion(fromResult, NullableType.GetUnderlyingType(op.SourceType))).ToList(); if (operatorsWithSourceEncompassingFromType.Any()) mostSpecificSource = FindMostEncompassedType(operatorsWithSourceEncompassingFromType.Select(op => op.SourceType)); else @@ -840,17 +851,29 @@ namespace ICSharpCode.NRefactory.CSharp.Resolver return Conversion.None; IType mostSpecificTarget; - if (operators.Any(op => op.TargetType.Equals(T0))) - mostSpecificTarget = T0; - else if (operators.Any(op => IsEncompassedBy(op.TargetType, T0))) - mostSpecificTarget = FindMostEncompassingType(operators.Where(op => IsEncompassedBy(op.TargetType, T0)).Select(op => op.TargetType)); + if (operators.Any(op => op.TargetType.Equals(toType))) + mostSpecificTarget = toType; + else if (operators.Any(op => IsEncompassedBy(op.TargetType, toType))) + mostSpecificTarget = FindMostEncompassingType(operators.Where(op => IsEncompassedBy(op.TargetType, toType)).Select(op => op.TargetType)); else mostSpecificTarget = FindMostEncompassedType(operators.Select(op => op.TargetType)); if (mostSpecificTarget == null) - return Conversion.None; + return NullableType.IsNullable(toType) ? UserDefinedExplicitConversion(fromResult, fromType, NullableType.GetUnderlyingType(toType)) : Conversion.None; - var selected = SelectOperator(mostSpecificSource, mostSpecificTarget, operators); - return selected != null ? Conversion.UserDefinedExplicitConversion(selected.Method, selected.IsLifted) : Conversion.None; + var selected = SelectOperator(mostSpecificSource, mostSpecificTarget, operators, true); + if (selected != null) { + if (selected.IsLifted && NullableType.IsNullable(toType)) { + // Prefer A -> B -> B? over A -> A? -> B? + var other = UserDefinedImplicitConversion(fromResult, fromType, NullableType.GetUnderlyingType(toType)); + if (other.IsValid) + return other; + } + return selected; + } + else if (NullableType.IsNullable(toType)) + return UserDefinedExplicitConversion(fromResult, fromType, NullableType.GetUnderlyingType(toType)); + else + return Conversion.None; } else { return Conversion.None; @@ -867,8 +890,8 @@ namespace ICSharpCode.NRefactory.CSharp.Resolver public OperatorInfo(IMethod method, IType sourceType, IType targetType, bool isLifted) { this.Method = method; - this.SourceType = NullableType.GetUnderlyingType(sourceType); - this.TargetType = NullableType.GetUnderlyingType(targetType); + this.SourceType = sourceType; + this.TargetType = targetType; this.IsLifted = isLifted; } } @@ -900,9 +923,8 @@ namespace ICSharpCode.NRefactory.CSharp.Resolver } // Try if the operator is applicable in lifted form: bool isApplicableInLiftedForm = false; - if (!isApplicable && NullableType.IsNonNullableValueType(sourceType)) { - // We don't bother checking if the operator is applicable in lifted form if it is already - // applicable in normal form, because the normal form will always be preferred. + if (NullableType.IsNonNullableValueType(sourceType)) { + // An operator can be applicable in both lifted and non-lifted form in case of explicit conversions IType liftedSourceType = NullableType.Create(compilation, sourceType); IType liftedTargetType = NullableType.IsNonNullableValueType(targetType) ? NullableType.Create(compilation, targetType) : targetType; if (isExplicit) { diff --git a/ICSharpCode.NRefactory.Tests/CSharp/Resolver/ConversionsTest.cs b/ICSharpCode.NRefactory.Tests/CSharp/Resolver/ConversionsTest.cs index 3179de03fe..f419b686db 100644 --- a/ICSharpCode.NRefactory.Tests/CSharp/Resolver/ConversionsTest.cs +++ b/ICSharpCode.NRefactory.Tests/CSharp/Resolver/ConversionsTest.cs @@ -1045,7 +1045,25 @@ class Test { Assert.IsTrue(c.IsUserDefined); Assert.AreEqual("ui", c.Method.Parameters[0].Name); } - + + [Test] + public void UserDefinedImplicitConversion_NullableUIntConstant() { + string program = @"using System; +class Convertible { + public static implicit operator Convertible(long? l) {return new Convertible(); } + public static implicit operator Convertible(uint? ui) {return new Convertible(); } +} +class Test { + public void M() { + Convertible a = $33$; + } +}"; + var c = GetConversion(program); + Assert.IsTrue(c.IsValid); + Assert.IsTrue(c.IsUserDefined); + Assert.AreEqual("ui", c.Method.Parameters[0].Name); + } + [Test] public void UserDefinedImplicitConversion_UseShortResult_BecauseNullableCannotBeUnpacked() { diff --git a/ICSharpCode.NRefactory.Tests/CSharp/Resolver/ExplicitConversionsTest.cs b/ICSharpCode.NRefactory.Tests/CSharp/Resolver/ExplicitConversionsTest.cs index 6c52f662b0..93652cebb0 100644 --- a/ICSharpCode.NRefactory.Tests/CSharp/Resolver/ExplicitConversionsTest.cs +++ b/ICSharpCode.NRefactory.Tests/CSharp/Resolver/ExplicitConversionsTest.cs @@ -686,7 +686,7 @@ class Test { Assert.IsFalse(rr.Conversion.IsValid); } - [Test, Ignore("We detect an ambiguity where csc and mcs can compile the code; but given that csc and mcs compile the code differently, the ambiguity may be an acceptable choice...")] + [Test] public void UserDefinedExplicitConversion_DefinedNullableTakesPrecedenceOverLifted() { string program = @"using System; struct Convertible { @@ -698,12 +698,6 @@ class Test { a = $(Convertible?)(int?)33$; } }"; - // There are three applicable conversions in this test: - // 1) int? -> Convertible? via lifted form of the first user-defined operator - // 2) int? -> Convertible? via second user-defined operator - // 3) int? -> int -> Convertible -> Convertible? (explicit nullable, first user defined, implicit nullable) - // csc picks option 2; mcs picks option 1. - // NRefactory currently fails with an ambiguity between 2 and 3. var rr = Resolve(program); Assert.IsTrue(rr.Conversion.IsValid); Assert.IsTrue(rr.Conversion.IsUserDefined); @@ -728,5 +722,60 @@ class Test { Assert.IsTrue(rr.Conversion.IsUserDefined); Assert.AreEqual("ui", rr.Conversion.Method.Parameters[0].Name); } + + [Test] + public void UserDefinedExplicitConversion_NullableUIntConstant() { + string program = @"using System; +class Convertible { + public static explicit operator Convertible(long? l) {return new Convertible(); } + public static explicit operator Convertible(uint? ui) {return new Convertible(); } +} +class Test { + public void M() { + Convertible a = $(Convertible)33$; + } +}"; + var rr = Resolve(program); + Assert.IsTrue(rr.Conversion.IsValid); + Assert.IsTrue(rr.Conversion.IsUserDefined); + Assert.AreEqual("ui", rr.Conversion.Method.Parameters[0].Name); + } + + [Test] + public void UseDefinedExplicitConversion_Lifted() { + string program = @" +struct Convertible { + public static explicit operator Convertible(int i) { return new Convertible(); } +} +class Test { + public void M(int? i) { + a = $(Convertible?)i$; + } +}"; + var rr = Resolve(program); + Assert.IsTrue(rr.Conversion.IsValid); + Assert.IsTrue(rr.Conversion.IsUserDefined); + Assert.IsTrue(rr.Conversion.IsLifted); + Assert.IsTrue(rr.Input is LocalResolveResult); + } + + [Test] + public void UserDefinedImplicitConversion_UseShortResult_X() { + string program = @"using System; +class Test { + public static implicit operator short(Test d) { return 0; } + public static implicit operator byte?(Test d) { return 0; } +} +class Program { + public static void Main(string[] args) + { + int? x = $new Test()$; + } +}"; + var c = GetConversion(program); + Assert.IsTrue(c.IsValid); + Assert.IsTrue(c.IsUserDefined); + Assert.AreEqual("System.Int16", c.Method.ReturnType.FullName); + } } }