diff --git a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj index 51d24fc07..51ed72689 100644 --- a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj +++ b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj @@ -89,6 +89,7 @@ + diff --git a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs index 6c66f55c3..e7786ac67 100644 --- a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs @@ -425,6 +425,12 @@ namespace ICSharpCode.Decompiler.Tests RunForLibrary(cscOptions: cscOptions); } + [Test] + public void UserDefinedConversions([ValueSource(nameof(defaultOptions))] CompilerOptions cscOptions) + { + RunForLibrary(cscOptions: cscOptions); + } + [Test] public void Discards([ValueSource(nameof(roslynOnlyOptions))] CompilerOptions cscOptions) { diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/UserDefinedConversions.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/UserDefinedConversions.cs new file mode 100644 index 000000000..a91ca7a82 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/UserDefinedConversions.cs @@ -0,0 +1,134 @@ +// Copyright (c) 2019 Daniel Grunwald +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty +{ + internal class T01Issue1574 + { + private struct A + { + private bool val; + + public static implicit operator bool(A a) + { + return a.val; + } + } + + private struct C + { + private int val; + + public static implicit operator C(bool b) + { + return default(C); + } + } + + private C ChainedConversion() + { + return (bool)default(A); + } + + public void Call_Overloaded() + { + Overloaded((bool)default(A)); + } + + private void Overloaded(A a) + { + } + + private void Overloaded(bool a) + { + } + } + + internal class T02BothDirectAndChainedConversionPossible + { + private struct A + { + private bool val; + + public static implicit operator bool(A a) + { + return a.val; + } + } + + private struct C + { + private int val; + + public static implicit operator C(bool b) + { + return default(C); + } + + public static implicit operator C(A a) + { + return default(C); + } + + public static bool operator ==(C a, C b) + { + return true; + } + public static bool operator !=(C a, C b) + { + return false; + } + } + + private C DirectConvert(A a) + { + return a; + } + + private C IndirectConvert(A a) + { + return (bool)a; + } + + private C? LiftedDirectConvert(A? a) + { + return a; + } + + private C? LiftedIndirectConvert(A? a) + { + return (bool?)a; + } + + private bool Compare(A a, C c) + { + return a == c; + } + + private void LiftedCompare(A? a, C? c) + { + UseBool(a == c); + UseBool(a == default(C)); + UseBool(c == default(A)); + } + + private void UseBool(bool b) + { + } + } +} diff --git a/ICSharpCode.Decompiler/CSharp/Annotations.cs b/ICSharpCode.Decompiler/CSharp/Annotations.cs index 064da15f2..ea813e554 100644 --- a/ICSharpCode.Decompiler/CSharp/Annotations.cs +++ b/ICSharpCode.Decompiler/CSharp/Annotations.cs @@ -240,4 +240,18 @@ namespace ICSharpCode.Decompiler.CSharp this.Leave = leave; } } + + /// + /// Annotates an expression when an implicit user-defined conversion was omitted. + /// + public class ImplicitConversionAnnotation + { + public readonly ConversionResolveResult ConversionResolveResult; + public IType TargetType => ConversionResolveResult.Type; + + public ImplicitConversionAnnotation(ConversionResolveResult conversionResolveResult) + { + this.ConversionResolveResult = conversionResolveResult; + } + } } diff --git a/ICSharpCode.Decompiler/CSharp/TranslatedExpression.cs b/ICSharpCode.Decompiler/CSharp/TranslatedExpression.cs index e0d3dbae6..e270f5563 100644 --- a/ICSharpCode.Decompiler/CSharp/TranslatedExpression.cs +++ b/ICSharpCode.Decompiler/CSharp/TranslatedExpression.cs @@ -191,7 +191,11 @@ namespace ICSharpCode.Decompiler.CSharp conversion.Input.Type, type, targetType )) { - return this.UnwrapChild(cast.Expression); + var result = this.UnwrapChild(cast.Expression); + if (conversion.Conversion.IsUserDefined) { + result.Expression.AddAnnotation(new ImplicitConversionAnnotation(conversion)); + } + return result; } else if (Expression is ObjectCreateExpression oce && conversion.Conversion.IsMethodGroupConversion && oce.Arguments.Count == 1 && expressionBuilder.settings.UseImplicitMethodGroupConversion) { return this.UnwrapChild(oce.Arguments.Single()); @@ -211,6 +215,18 @@ namespace ICSharpCode.Decompiler.CSharp if (targetType.Kind == TypeKind.Unknown || targetType.Kind == TypeKind.Void || targetType.Kind == TypeKind.None) { return this; // don't attempt to insert cast to '?' or 'void' as these are not valid. } + var convAnnotation = this.Expression.Annotation(); + if (convAnnotation != null) { + // If an implicit user-defined conversion was stripped from this expression; + // it needs to be re-introduced before we can apply other casts to this expression. + // This happens when the CallBuilder discovers that the conversion is necessary in + // order to choose the correct overload. + this.Expression.RemoveAnnotations(); + return new CastExpression(expressionBuilder.ConvertType(convAnnotation.TargetType), Expression) + .WithoutILInstruction() + .WithRR(convAnnotation.ConversionResolveResult) + .ConvertTo(targetType, expressionBuilder, checkForOverflow, allowImplicitConversion); + } if (Expression is TupleExpression tupleExpr && targetType is TupleType targetTupleType && tupleExpr.Elements.Count == targetTupleType.ElementTypes.Length) { @@ -231,8 +247,9 @@ namespace ICSharpCode.Decompiler.CSharp } var compilation = expressionBuilder.compilation; var conversions = Resolver.CSharpConversions.Get(compilation); - if (ResolveResult is ConversionResolveResult conv && Expression is CastExpression cast2 && - CastCanBeMadeImplicit(conversions, conv.Conversion, conv.Input.Type, type, targetType)) + if (ResolveResult is ConversionResolveResult conv && Expression is CastExpression cast2 + && !conv.Conversion.IsUserDefined + && CastCanBeMadeImplicit(conversions, conv.Conversion, conv.Input.Type, type, targetType)) { var unwrapped = this.UnwrapChild(cast2.Expression); if (allowImplicitConversion)