diff --git a/ICSharpCode.Decompiler.Tests/Documentation/IdStringProviderTests.cs b/ICSharpCode.Decompiler.Tests/Documentation/IdStringProviderTests.cs index 78c65be6c8e..3d4395334b2 100644 --- a/ICSharpCode.Decompiler.Tests/Documentation/IdStringProviderTests.cs +++ b/ICSharpCode.Decompiler.Tests/Documentation/IdStringProviderTests.cs @@ -553,9 +553,7 @@ public void TearDown() roslynIdMap = null; if (tempDllPath != null && File.Exists(tempDllPath)) { - try - { File.Delete(tempDllPath); } - catch { /* best effort cleanup */ } + Tester.RepeatOnIOError(() => File.Delete(tempDllPath)); } } diff --git a/ICSharpCode.Decompiler.Tests/Helpers/Tester.cs b/ICSharpCode.Decompiler.Tests/Helpers/Tester.cs index a56830f3fb0..fb608658ffe 100644 --- a/ICSharpCode.Decompiler.Tests/Helpers/Tester.cs +++ b/ICSharpCode.Decompiler.Tests/Helpers/Tester.cs @@ -1161,26 +1161,37 @@ public static async Task RunAndCompareOutput(string testFileName, string outputF } } - internal static void RepeatOnIOError(Action action, int numTries = 5) + /// + /// Retries an IO operation that a virus scanner, the indexer or a compiler that has only + /// just exited can still hold a handle on, backing off between attempts. Every caller is + /// deleting a temp file, so a failure is reported and swallowed: the file is left behind, + /// which costs nothing, where throwing out of a fixture teardown reports an error for a + /// run that otherwise passed. + /// + internal static void RepeatOnIOError(Action action, int numTries = 8) { - for (int i = 0; i < numTries - 1; i++) + Exception lastError = null; + int delay = 10; + for (int i = 0; i < numTries; i++) { try { action(); return; } - catch (IOException) + catch (IOException ex) { + lastError = ex; } - catch (UnauthorizedAccessException) + catch (UnauthorizedAccessException ex) { // potential virus scanner problem + lastError = ex; } - Thread.Sleep(10); + Thread.Sleep(delay); + delay = Math.Min(delay * 2, 500); } - // If the last try still fails, don't catch the exception - action(); + TestContext.Out.WriteLine($"Cleanup could not delete a temp file after {numTries} tries, leaving it behind: {lastError?.Message}"); } public static async Task SignAssembly(string assemblyPath, string keyFilePath) diff --git a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj index 7bbdf3087d9..320d4badbf6 100644 --- a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj +++ b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj @@ -273,6 +273,10 @@ + + + + diff --git a/ICSharpCode.Decompiler.Tests/Output/LocationsInAstTests.cs b/ICSharpCode.Decompiler.Tests/Output/LocationsInAstTests.cs index 50eeee9373b..34db1dfb954 100644 --- a/ICSharpCode.Decompiler.Tests/Output/LocationsInAstTests.cs +++ b/ICSharpCode.Decompiler.Tests/Output/LocationsInAstTests.cs @@ -17,6 +17,7 @@ // DEALINGS IN THE SOFTWARE. using System; +using System.Collections.Generic; using System.IO; using System.Linq; @@ -55,6 +56,7 @@ public class LocationsInAstTests const string SequencePointSampleName = "ICSharpCode.Decompiler.Tests.TestCases.LocationsInAst.SequencePointSample"; static CompilerResults compiledSamples; + static readonly List openedModules = new List(); [OneTimeSetUp] public void CompileSamples() @@ -66,6 +68,9 @@ public void CompileSamples() [OneTimeTearDown] public void DeleteCompiledSamples() { + // The module keeps the assembly mapped, so it has to go before the file can. + foreach (var module in openedModules) + module.Dispose(); compiledSamples?.DeleteTempFiles(); } @@ -73,6 +78,7 @@ static CSharpDecompiler CreateDecompiler(out DecompilerSettings settings) { string assemblyPath = compiledSamples.PathToAssembly; var module = new PEFile(assemblyPath); + openedModules.Add(module); var resolver = new UniversalAssemblyResolver(assemblyPath, false, module.Metadata.DetectTargetFrameworkId()); settings = new DecompilerSettings(); return new CSharpDecompiler(module, resolver, settings); diff --git a/ICSharpCode.Decompiler.Tests/ProjectDecompiler/WpfProjectExportTests.cs b/ICSharpCode.Decompiler.Tests/ProjectDecompiler/WpfProjectExportTests.cs index 50b30d5561c..fe412204905 100644 --- a/ICSharpCode.Decompiler.Tests/ProjectDecompiler/WpfProjectExportTests.cs +++ b/ICSharpCode.Decompiler.Tests/ProjectDecompiler/WpfProjectExportTests.cs @@ -25,6 +25,7 @@ using ICSharpCode.Decompiler.CSharp.ProjectDecompiler; using ICSharpCode.Decompiler.Metadata; +using ICSharpCode.Decompiler.Tests.Helpers; using ICSharpCode.Decompiler.TypeSystem; using Microsoft.CodeAnalysis; @@ -107,7 +108,7 @@ public void TearDown() foreach (var module in openedModules) module.Dispose(); if (Directory.Exists(tempDirectory)) - Directory.Delete(tempDirectory, recursive: true); + Tester.RepeatOnIOError(() => Directory.Delete(tempDirectory, recursive: true)); } /// diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/QualifierTests.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/QualifierTests.cs index 2f5eb6f5fda..8f3a9a99b23 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/QualifierTests.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/QualifierTests.cs @@ -139,6 +139,46 @@ public void BaseQualifiers() } } + internal class OverridingParent : Parent + { +#if LEGACY_CSC + public virtual int Prop { + get { + return 1; + } + } +#else + public virtual int Prop => 1; +#endif + + public override void Virtual() + { + } + } + + internal class OverridingChild : OverridingParent + { +#if LEGACY_CSC + public override int Prop { + get { + return 2; + } + } +#else + public override int Prop => 2; +#endif + + // Neither member is overridden here, so the unqualified spelling would bind to the + // same one and the qualifier looks redundant - but Virtual is an override and Prop is + // overridden further down, so dropping it would dispatch to this type's member + // instead of the one the base call names. + public void BaseQualifiersOnOverriddenMembers() + { + base.Virtual(); + base.Prop.ToString(); + } + } + #pragma warning disable CS8981 private class i { diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Ugly/BaseQualifier.Expected.cs b/ICSharpCode.Decompiler.Tests/TestCases/Ugly/BaseQualifier.Expected.cs new file mode 100644 index 00000000000..c711ecab68d --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Ugly/BaseQualifier.Expected.cs @@ -0,0 +1,65 @@ +using System; + +namespace ICSharpCode.Decompiler.Tests.TestCases.Ugly; + +public class BaseQualifierBase : BaseQualifierRoot +{ + public int NonVirtualProperty { get; set; } + public virtual int VirtualProperty { get; set; } + public override int AbstractProperty { get; set; } + public override int OverriddenProperty { get; set; } + public sealed override int SealedProperty { get; set; } + + public void NonVirtualMethod() + { + } + + public virtual void VirtualMethod() + { + } +} + +public class BaseQualifierDerived : BaseQualifierBase +{ + public int ReadNonVirtualProperty() + { + return NonVirtualProperty; + } + + public int ReadSealedProperty() + { + return SealedProperty; + } + + public Action NonVirtualMethodGroup() + { + return NonVirtualMethod; + } + + public int ReadVirtualProperty() + { + return base.VirtualProperty; + } + + public int ReadAbstractProperty() + { + return base.AbstractProperty; + } + + public int ReadOverriddenProperty() + { + return base.OverriddenProperty; + } + + public Action VirtualMethodGroup() + { + return base.VirtualMethod; + } +} + +public abstract class BaseQualifierRoot +{ + public abstract int AbstractProperty { get; set; } + public virtual int OverriddenProperty { get; set; } + public virtual int SealedProperty { get; set; } +} diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Ugly/BaseQualifier.cs b/ICSharpCode.Decompiler.Tests/TestCases/Ugly/BaseQualifier.cs new file mode 100644 index 00000000000..8348dd0f34d --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Ugly/BaseQualifier.cs @@ -0,0 +1,68 @@ +using System; + +namespace ICSharpCode.Decompiler.Tests.TestCases.Ugly +{ + public abstract class BaseQualifierRoot + { + public abstract int AbstractProperty { get; set; } + public virtual int OverriddenProperty { get; set; } + public virtual int SealedProperty { get; set; } + } + + public class BaseQualifierBase : BaseQualifierRoot + { + public int NonVirtualProperty { get; set; } + public virtual int VirtualProperty { get; set; } + public override int AbstractProperty { get; set; } + public override int OverriddenProperty { get; set; } + public sealed override int SealedProperty { get; set; } + + public void NonVirtualMethod() + { + } + + public virtual void VirtualMethod() + { + } + } + + public class BaseQualifierDerived : BaseQualifierBase + { + // Dropping "base." leaves the reference dispatching virtually. That reaches the same + // member unless the member can be overridden, so only the overridable ones keep it. + public int ReadNonVirtualProperty() + { + return base.NonVirtualProperty; + } + + public int ReadSealedProperty() + { + return base.SealedProperty; + } + + public Action NonVirtualMethodGroup() + { + return base.NonVirtualMethod; + } + + public int ReadVirtualProperty() + { + return base.VirtualProperty; + } + + public int ReadAbstractProperty() + { + return base.AbstractProperty; + } + + public int ReadOverriddenProperty() + { + return base.OverriddenProperty; + } + + public Action VirtualMethodGroup() + { + return base.VirtualMethod; + } + } +} diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Ugly/QualifiedMethodGroup.Expected.cs b/ICSharpCode.Decompiler.Tests/TestCases/Ugly/QualifiedMethodGroup.Expected.cs new file mode 100644 index 00000000000..2a98fe2f4bc --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Ugly/QualifiedMethodGroup.Expected.cs @@ -0,0 +1,20 @@ +using System; + +namespace ICSharpCode.Decompiler.Tests.TestCases.Ugly; + +public class QualifiedMethodGroup +{ + public int Value; + + public event EventHandler Changed; + + public void Subscribe() + { + this.Changed += new EventHandler(this.OnChanged); + this.Value = 1; + } + + private void OnChanged(object sender, EventArgs e) + { + } +} diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Ugly/QualifiedMethodGroup.cs b/ICSharpCode.Decompiler.Tests/TestCases/Ugly/QualifiedMethodGroup.cs new file mode 100644 index 00000000000..b02a5bf06ff --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Ugly/QualifiedMethodGroup.cs @@ -0,0 +1,21 @@ +using System; + +namespace ICSharpCode.Decompiler.Tests.TestCases.Ugly +{ + public class QualifiedMethodGroup + { + public event EventHandler Changed; + + public int Value; + + public void Subscribe() + { + Changed += OnChanged; + Value = 1; + } + + private void OnChanged(object sender, EventArgs e) + { + } + } +} diff --git a/ICSharpCode.Decompiler.Tests/TypeSystem/DuplicateAssemblyReferenceTests.cs b/ICSharpCode.Decompiler.Tests/TypeSystem/DuplicateAssemblyReferenceTests.cs index d3def0338cb..b28954cdc07 100644 --- a/ICSharpCode.Decompiler.Tests/TypeSystem/DuplicateAssemblyReferenceTests.cs +++ b/ICSharpCode.Decompiler.Tests/TypeSystem/DuplicateAssemblyReferenceTests.cs @@ -17,6 +17,7 @@ // DEALINGS IN THE SOFTWARE. using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -113,6 +114,7 @@ .method public hidebysig virtual instance void Run(class [Lib]Param p) cil manag string directory; string mainAssemblyPath; + readonly List openedModules = new List(); [OneTimeSetUp] public async Task SetUp() @@ -129,7 +131,11 @@ public async Task SetUp() [OneTimeTearDown] public void TearDown() { - Directory.Delete(directory, recursive: true); + // A PEFile keeps the assembly mapped, so the directory cannot be removed while any + // of the modules these tests opened is still around. + foreach (var module in openedModules) + module.Dispose(); + Tester.RepeatOnIOError(() => Directory.Delete(directory, recursive: true)); } Task AssembleAsync(string name, string il) @@ -142,7 +148,8 @@ Task AssembleAsync(string name, string il) DecompilerTypeSystem CreateTypeSystem() { var mainModule = new PEFile(mainAssemblyPath); - return new DecompilerTypeSystem(mainModule, new VersionedResolver(directory)); + openedModules.Add(mainModule); + return new DecompilerTypeSystem(mainModule, new VersionedResolver(directory, openedModules)); } [Test] @@ -179,10 +186,12 @@ class VersionedResolver : IAssemblyResolver static readonly string runtimeDirectory = Path.GetDirectoryName(typeof(object).Assembly.Location); readonly string directory; + readonly List openedModules; - public VersionedResolver(string directory) + public VersionedResolver(string directory, List openedModules) { this.directory = directory; + this.openedModules = openedModules; } public MetadataFile Resolve(IAssemblyReference reference) @@ -190,7 +199,11 @@ public MetadataFile Resolve(IAssemblyReference reference) string path = reference.Name == "Lib" ? Path.Combine(directory, $"Lib.v{reference.Version.Major}.dll") : Path.Combine(runtimeDirectory, reference.Name + ".dll"); - return File.Exists(path) ? new PEFile(path) : null; + if (!File.Exists(path)) + return null; + var module = new PEFile(path); + openedModules.Add(module); + return module; } public MetadataFile ResolveModule(MetadataFile mainModule, string moduleName) => null; diff --git a/ICSharpCode.Decompiler.Tests/TypeSystem/TypeForwarderResolutionTests.cs b/ICSharpCode.Decompiler.Tests/TypeSystem/TypeForwarderResolutionTests.cs index b6ebed6b3c7..c0e21b5b930 100644 --- a/ICSharpCode.Decompiler.Tests/TypeSystem/TypeForwarderResolutionTests.cs +++ b/ICSharpCode.Decompiler.Tests/TypeSystem/TypeForwarderResolutionTests.cs @@ -17,6 +17,7 @@ // DEALINGS IN THE SOFTWARE. using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -173,6 +174,7 @@ .method public hidebysig instance void UseUndeclared(class [Shim2]Ns.U u) cil ma "; string inputDirectory; + readonly List openedModules = new List(); string frameworkDirectory; string mainAssemblyPath; @@ -202,7 +204,9 @@ public async Task SetUp() [OneTimeTearDown] public void TearDown() { - Directory.Delete(Path.GetDirectoryName(inputDirectory), recursive: true); + foreach (var module in openedModules) + module.Dispose(); + Tester.RepeatOnIOError(() => Directory.Delete(Path.GetDirectoryName(inputDirectory), recursive: true)); } static Task AssembleAsync(string directory, string name, string il) @@ -215,12 +219,17 @@ static Task AssembleAsync(string directory, string name, string il) DecompilerTypeSystem CreateTypeSystem() { var mainModule = new PEFile(mainAssemblyPath); + openedModules.Add(mainModule); // The target framework decides which probe runs first, and the bug only shows on the // .NET Core path finder, which the .NET Standard identifier selects. var resolver = new UniversalAssemblyResolver(mainAssemblyPath, throwOnError: false, ".NETStandard,Version=v2.0"); resolver.AddSearchDirectory(frameworkDirectory); - return new DecompilerTypeSystem(mainModule, resolver); + var typeSystem = new DecompilerTypeSystem(mainModule, resolver); + // The resolver opens the framework and implementation assemblies as it probes; every + // one of them keeps its file mapped until it is disposed. + openedModules.AddRange(typeSystem.Modules.Select(m => m.MetadataFile).Where(f => f != null && f != mainModule)); + return typeSystem; } IParameter GetParameterOf(DecompilerTypeSystem typeSystem, string methodName) diff --git a/ICSharpCode.Decompiler.Tests/UglyTestRunner.cs b/ICSharpCode.Decompiler.Tests/UglyTestRunner.cs index 15127eb5c2b..b6a70a46b0c 100644 --- a/ICSharpCode.Decompiler.Tests/UglyTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/UglyTestRunner.cs @@ -218,6 +218,24 @@ public async Task TopLevelProgramAsync([ValueSource(nameof(topLevelProgramOption await Run(cscOptions: cscOptions); } + // AlwaysQualifyMemberReferences is turned on for the WinForms InitializeComponent method, + // which is also decompiled with implicit method group conversion off - so the method group + // has to be qualified like every other member reference. + [Test] + public async Task QualifiedMethodGroup([ValueSource(nameof(roslynOnlyOptions))] CompilerOptions cscOptions) + { + await RunForLibrary(cscOptions: cscOptions, decompilerSettings: new DecompilerSettings { + AlwaysQualifyMemberReferences = true, + UseImplicitMethodGroupConversion = false + }); + } + + [Test] + public async Task BaseQualifier([ValueSource(nameof(roslynOnlyOptions))] CompilerOptions cscOptions) + { + await RunForLibrary(cscOptions: cscOptions); + } + async Task RunForLibrary([CallerMemberName] string testName = null, CompilerOptions cscOptions = CompilerOptions.None, DecompilerSettings decompilerSettings = null) { await Run(testName, cscOptions | CompilerOptions.Library, decompilerSettings); diff --git a/ICSharpCode.Decompiler.Tests/UniversalAssemblyResolverTests.cs b/ICSharpCode.Decompiler.Tests/UniversalAssemblyResolverTests.cs index 2630cfdfd59..b73271fc4d0 100644 --- a/ICSharpCode.Decompiler.Tests/UniversalAssemblyResolverTests.cs +++ b/ICSharpCode.Decompiler.Tests/UniversalAssemblyResolverTests.cs @@ -19,6 +19,7 @@ using System.IO; using ICSharpCode.Decompiler.Metadata; +using ICSharpCode.Decompiler.Tests.Helpers; using NUnit.Framework; @@ -44,7 +45,7 @@ public void SetUp() public void TearDown() { if (Directory.Exists(gacDirectory)) - Directory.Delete(gacDirectory, recursive: true); + Tester.RepeatOnIOError(() => Directory.Delete(gacDirectory, recursive: true)); } string AddAssembly(string name, string version, string publicKeyToken, string culture = "") diff --git a/ICSharpCode.Decompiler/CSharp/CallBuilder.cs b/ICSharpCode.Decompiler/CSharp/CallBuilder.cs index 1836d8b7f12..b6e1dc244db 100644 --- a/ICSharpCode.Decompiler/CSharp/CallBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/CallBuilder.cs @@ -37,154 +37,155 @@ namespace ICSharpCode.Decompiler.CSharp { - struct CallBuilder + internal struct ExpectedTargetDetails + { + public OpCode CallOpCode; + public bool NeedsBoxingConversion; + } + + internal struct ArgumentList { - struct ExpectedTargetDetails + public TranslatedExpression[] Arguments; + public IParameter[] ExpectedParameters; + public string[] ParameterNames; + public string[]? ArgumentNames; + public int FirstOptionalArgumentIndex; + public BitSet IsPrimitiveValue; + public IReadOnlyList? ArgumentToParameterMap; + + public bool AddNamesToPrimitiveValues; + public bool UseImplicitlyTypedOut; + public bool IsExpandedForm; + public int Length => Arguments.Length; + + private int GetActualArgumentCount() { - public OpCode CallOpCode; - public bool NeedsBoxingConversion; + if (FirstOptionalArgumentIndex < 0) + return Arguments.Length; + return FirstOptionalArgumentIndex; } - struct ArgumentList + public string[]? GetArgumentNames(int skipCount = 0) { - public TranslatedExpression[] Arguments; - public IParameter[] ExpectedParameters; - public string[] ParameterNames; - public string[]? ArgumentNames; - public int FirstOptionalArgumentIndex; - public BitSet IsPrimitiveValue; - public IReadOnlyList? ArgumentToParameterMap; - - public bool AddNamesToPrimitiveValues; - public bool UseImplicitlyTypedOut; - public bool IsExpandedForm; - public int Length => Arguments.Length; - - private int GetActualArgumentCount() + string[]? argumentNames = ArgumentNames; + if (AddNamesToPrimitiveValues && IsPrimitiveValue.Any() && !IsExpandedForm + && !ParameterNames.Any(string.IsNullOrEmpty)) { - if (FirstOptionalArgumentIndex < 0) - return Arguments.Length; - return FirstOptionalArgumentIndex; - } - - public string[]? GetArgumentNames(int skipCount = 0) - { - string[]? argumentNames = ArgumentNames; - if (AddNamesToPrimitiveValues && IsPrimitiveValue.Any() && !IsExpandedForm - && !ParameterNames.Any(string.IsNullOrEmpty)) + Debug.Assert(skipCount == 0); + if (argumentNames == null) { - Debug.Assert(skipCount == 0); - if (argumentNames == null) - { - argumentNames = new string[Arguments.Length]; - } + argumentNames = new string[Arguments.Length]; + } - for (int i = 0; i < Arguments.Length; i++) + for (int i = 0; i < Arguments.Length; i++) + { + if (IsPrimitiveValue[i] && argumentNames[i] == null) { - if (IsPrimitiveValue[i] && argumentNames[i] == null) - { - argumentNames[i] = ParameterNames[i]; - } + argumentNames[i] = ParameterNames[i]; } } - - return argumentNames; } - public IList GetArgumentResolveResults(int skipCount = 0) - { - var expectedParameters = ExpectedParameters; - var useImplicitlyTypedOut = UseImplicitlyTypedOut; + return argumentNames; + } - return Arguments - .SelectWithIndex(GetResolveResult) - .Skip(skipCount) - .Take(GetActualArgumentCount()) - .ToArray(); + public IList GetArgumentResolveResults(int skipCount = 0) + { + var expectedParameters = ExpectedParameters; + var useImplicitlyTypedOut = UseImplicitlyTypedOut; - ResolveResult GetResolveResult(int index, TranslatedExpression expression) - { - var param = expectedParameters[index]; - if (useImplicitlyTypedOut && param.ReferenceKind == ReferenceKind.Out && expression.Type is ByReferenceType brt) - return new OutVarResolveResult(brt.ElementType); - return expression.ResolveResult; - } - } + return Arguments + .SelectWithIndex(GetResolveResult) + .Skip(skipCount) + .Take(GetActualArgumentCount()) + .ToArray(); - public IList GetArgumentResolveResultsDirect(int skipCount = 0) + ResolveResult GetResolveResult(int index, TranslatedExpression expression) { - return Arguments - .Skip(skipCount) - .Take(GetActualArgumentCount()) - .Select(a => a.ResolveResult) - .ToArray(); + var param = expectedParameters[index]; + if (useImplicitlyTypedOut && param.ReferenceKind == ReferenceKind.Out && expression.Type is ByReferenceType brt) + return new OutVarResolveResult(brt.ElementType); + return expression.ResolveResult; } + } + + public IList GetArgumentResolveResultsDirect(int skipCount = 0) + { + return Arguments + .Skip(skipCount) + .Take(GetActualArgumentCount()) + .Select(a => a.ResolveResult) + .ToArray(); + } - public IEnumerable GetArgumentExpressions(int skipCount = 0) + public IEnumerable GetArgumentExpressions(int skipCount = 0) + { + var argumentNames = GetArgumentNames(skipCount); + int argumentCount = GetActualArgumentCount(); + var useImplicitlyTypedOut = UseImplicitlyTypedOut; + if (argumentNames == null) { - var argumentNames = GetArgumentNames(skipCount); - int argumentCount = GetActualArgumentCount(); - var useImplicitlyTypedOut = UseImplicitlyTypedOut; - if (argumentNames == null) - { - return Arguments.Skip(skipCount).Take(argumentCount).Select(arg => AddAnnotations(arg.Expression)); - } - else - { - Debug.Assert(skipCount == 0); - return Arguments.Take(argumentCount).Zip(argumentNames.Take(argumentCount), - (arg, name) => { - if (name == null) - return AddAnnotations(arg.Expression); - else - return new NamedArgumentExpression(name, AddAnnotations(arg.Expression)); - }); - } + return Arguments.Skip(skipCount).Take(argumentCount).Select(arg => AddAnnotations(arg.Expression)); + } + else + { + Debug.Assert(skipCount == 0); + return Arguments.Take(argumentCount).Zip(argumentNames.Take(argumentCount), + (arg, name) => { + if (name == null) + return AddAnnotations(arg.Expression); + else + return new NamedArgumentExpression(name, AddAnnotations(arg.Expression)); + }); + } - Expression AddAnnotations(Expression expression) - { - if (!useImplicitlyTypedOut) - return expression; - if (expression.GetResolveResult() is ByReferenceResolveResult { ReferenceKind: ReferenceKind.Out } brrr) - { - expression.AddAnnotation(UseImplicitlyTypedOutAnnotation.Instance); - } + Expression AddAnnotations(Expression expression) + { + if (!useImplicitlyTypedOut) return expression; + if (expression.GetResolveResult() is ByReferenceResolveResult { ReferenceKind: ReferenceKind.Out } brrr) + { + expression.AddAnnotation(UseImplicitlyTypedOutAnnotation.Instance); } + return expression; } + } - public bool CanInferAnonymousTypePropertyNamesFromArguments() + public bool CanInferAnonymousTypePropertyNamesFromArguments() + { + for (int i = 0; i < Arguments.Length; i++) { - for (int i = 0; i < Arguments.Length; i++) + string? inferredName; + switch (Arguments[i].Expression) { - string? inferredName; - switch (Arguments[i].Expression) - { - case IdentifierExpression identifier: - inferredName = identifier.Identifier; - break; - case MemberReferenceExpression member: - inferredName = member.MemberName; - break; - default: - inferredName = null; - break; - } + case IdentifierExpression identifier: + inferredName = identifier.Identifier; + break; + case MemberReferenceExpression member: + inferredName = member.MemberName; + break; + default: + inferredName = null; + break; + } - if (inferredName != ExpectedParameters[i].Name) - { - return false; - } + if (inferredName != ExpectedParameters[i].Name) + { + return false; } - return true; } + return true; + } - [Conditional("DEBUG")] - public void CheckNoNamedOrOptionalArguments() - { - Debug.Assert(ArgumentToParameterMap == null && ArgumentNames == null && FirstOptionalArgumentIndex < 0); - } + [Conditional("DEBUG")] + public void CheckNoNamedOrOptionalArguments() + { + Debug.Assert(ArgumentToParameterMap == null && ArgumentNames == null && FirstOptionalArgumentIndex < 0); } + } + + struct CallBuilder + { readonly DecompilerSettings settings; readonly ExpressionBuilder expressionBuilder; @@ -504,7 +505,7 @@ public ExpressionWithResolveResult Build(OpCode callOpCode, IMethod method, if (method.IsAccessor && (method.AccessorOwner.SymbolKind == SymbolKind.Indexer || argumentList.ExpectedParameters.Length == allowedParamCount)) { argumentList.CheckNoNamedOrOptionalArguments(); - return HandleAccessorCall(expectedTargetDetails, method, target, argumentList.Arguments.ToList(), argumentList.ArgumentNames); + return HandleAccessorCall(expectedTargetDetails, method, target, argumentList); } if (IsDelegateEqualityComparison(method, argumentList.Arguments)) @@ -587,7 +588,7 @@ public ExpressionWithResolveResult Build(OpCode callOpCode, IMethod method, } var transform = GetRequiredTransformationsForCall(expectedTargetDetails, method, ref target, - ref argumentList, CallTransformation.All, out IParameterizedMember? foundMethod); + ref argumentList, ReferenceTransformation.All, out IParameterizedMember? foundMethod); // GetRequiredTransformationsForCall always assigns foundMethod (the resolved overload or 'method'). Debug.Assert(foundMethod != null); @@ -606,15 +607,15 @@ public ExpressionWithResolveResult Build(OpCode callOpCode, IMethod method, Expression targetExpr; string methodName = method.Name; AstNodeCollection typeArgumentList; - if ((transform & CallTransformation.NoNamedArgsForPrettiness) != 0) + if ((transform & ReferenceTransformation.NoNamedArgsForPrettiness) != 0) { argumentList.AddNamesToPrimitiveValues = false; } - if ((transform & CallTransformation.NoOptionalArgumentAllowed) != 0) + if ((transform & ReferenceTransformation.NoOptionalArgumentAllowed) != 0) { argumentList.FirstOptionalArgumentIndex = -1; } - if ((transform & CallTransformation.RequireTarget) != 0) + if ((transform & ReferenceTransformation.RequireTarget) != 0) { targetExpr = new MemberReferenceExpression(target.Expression, methodName); typeArgumentList = ((MemberReferenceExpression)targetExpr).TypeArguments; @@ -640,7 +641,7 @@ public ExpressionWithResolveResult Build(OpCode callOpCode, IMethod method, typeArgumentList = ((IdentifierExpression)targetExpr).TypeArguments; } - if ((transform & CallTransformation.RequireTypeArguments) != 0 && (!settings.AnonymousTypes || !method.TypeArguments.Any(a => a.ContainsAnonymousType()))) + if ((transform & ReferenceTransformation.RequireTypeArguments) != 0 && (!settings.AnonymousTypes || !method.TypeArguments.Any(a => a.ContainsAnonymousType()))) typeArgumentList.AddRange(method.TypeArguments.Select(expressionBuilder.ConvertType)); return new InvocationExpression(targetExpr, argumentList.GetArgumentExpressions()) .WithRR(new CSharpInvocationResolveResult(target.ResolveResult, foundMethod, @@ -751,9 +752,14 @@ public ExpressionWithResolveResult BuildCollectionInitializerExpression(OpCode c argumentList.ArgumentNames = null; argumentList.AddNamesToPrimitiveValues = false; argumentList.UseImplicitlyTypedOut = false; + // Collection initializer syntax has nowhere to put a target or type arguments, so + // casting the arguments is the only way left to pin the Add method down. var transform = GetRequiredTransformationsForCall(expectedTargetDetails, method, ref unused, - ref argumentList, CallTransformation.None, out _); - Debug.Assert((transform & ~(CallTransformation.NoOptionalArgumentAllowed | CallTransformation.NoNamedArgsForPrettiness)) == 0); + ref argumentList, + ReferenceTransformation.CastArguments | ReferenceTransformation.NoOptionalArgumentAllowed + | ReferenceTransformation.NoNamedArgsForPrettiness, + out _); + Debug.Assert((transform & ~(ReferenceTransformation.NoOptionalArgumentAllowed | ReferenceTransformation.NoNamedArgsForPrettiness)) == 0); // Calls with only one argument do not need an array initializer expression to wrap them. // Any special cases are handled by the caller (i.e., ExpressionBuilder.TranslateObjectAndCollectionInitializer) @@ -772,7 +778,7 @@ public ExpressionWithResolveResult BuildCollectionInitializerExpression(OpCode c skipCount = 0; } - if ((transform & CallTransformation.NoOptionalArgumentAllowed) != 0) + if ((transform & ReferenceTransformation.NoOptionalArgumentAllowed) != 0) argumentList.FirstOptionalArgumentIndex = -1; return new ArrayInitializerExpression(argumentList.GetArgumentExpressions(skipCount)) @@ -795,8 +801,7 @@ public ExpressionWithResolveResult BuildDictionaryInitializerExpression(OpCode c var argumentList = BuildArgumentList(expectedTargetDetails, target, method, 1, callArguments, null); var unused = new IdentifierExpression("initializedObject").WithRR(target).WithoutILInstruction(); - var assignment = HandleAccessorCall(expectedTargetDetails, method, unused, - argumentList.Arguments.ToList(), argumentList.ArgumentNames); + var assignment = HandleAccessorCall(expectedTargetDetails, method, unused, argumentList); if (((AssignmentExpression)assignment).Left is IndexerExpression indexer && indexer.Target is not null) indexer.Target.Remove(); @@ -1132,7 +1137,7 @@ private bool TransformParamsArgument(ExpectedTargetDetails expectedTargetDetails { expandedParameters.InsertRange(0, expectedParameters); expandedArguments.InsertRange(0, arguments); - if (IsUnambiguousCall(expectedTargetDetails, method, targetResolveResult, Empty.Array, + if (Disambiguator.IsUnambiguousCall(expressionBuilder, expectedTargetDetails, method, targetResolveResult, Empty.Array, expandedArguments.SelectArray(a => a.ResolveResult), argumentNames: null, firstOptionalArgumentIndex: -1, out _, out var bestCandidateIsExpandedForm) == OverloadResolutionErrors.None && bestCandidateIsExpandedForm) @@ -1228,380 +1233,65 @@ bool IsOptionalArgument(IParameter parameter, TranslatedExpression arg) return object.Equals(parameter.GetConstantValue(), arg.ResolveResult.ConstantValue); } - [Flags] - enum CallTransformation - { - None = 0, - RequireTarget = 1, - RequireTypeArguments = 2, - NoOptionalArgumentAllowed = 4, - /// - /// Add calls to AsRefReadOnly for in parameters that did not have an explicit DirectionExpression yet. - /// - EnforceExplicitIn = 8, - NoNamedArgsForPrettiness = 0x10, - All = 0x1f, - } - - private CallTransformation GetRequiredTransformationsForCall(ExpectedTargetDetails expectedTargetDetails, IMethod method, - ref TranslatedExpression target, ref ArgumentList argumentList, CallTransformation allowedTransforms, out IParameterizedMember? foundMethod) + private ReferenceTransformation GetRequiredTransformationsForCall(ExpectedTargetDetails expectedTargetDetails, IMethod method, + ref TranslatedExpression target, ref ArgumentList argumentList, ReferenceTransformation allowedTransforms, out IParameterizedMember? foundMethod) { - CallTransformation transform = CallTransformation.None; + ReferenceTransformation transform = ReferenceTransformation.None; // initialize requireTarget flag bool requireTarget; - ResolveResult? targetResolveResult; - if ((allowedTransforms & CallTransformation.RequireTarget) != 0) + if ((allowedTransforms & ReferenceTransformation.RequireTarget) != 0) { - if (settings.AlwaysQualifyMemberReferences || expressionBuilder.HidesVariableWithName(method.Name)) + if (method.IsLocalFunction) + { + // A local function is never reached through a target. + requireTarget = settings.AlwaysQualifyMemberReferences + || expressionBuilder.HidesVariableWithName(method.Name); + } + else if (method.Name == ".ctor" || method.Name == ".cctor") { + // Always use target for base/this-ctor-call, the constructor initializer pattern depends on this requireTarget = true; } else { - if (method.IsLocalFunction) - requireTarget = false; - else if (method.IsStatic) - requireTarget = !expressionBuilder.IsCurrentOrContainingType(method.DeclaringTypeDefinition) || method.Name == ".cctor"; - else if (method.Name == ".ctor") - requireTarget = true; // always use target for base/this-ctor-call, the constructor initializer pattern depends on this - else if (target.Expression is BaseReferenceExpression) - requireTarget = (expectedTargetDetails.CallOpCode != OpCode.CallVirt && method.IsVirtual); - else - requireTarget = target.Expression is not ThisReferenceExpression; + requireTarget = expressionBuilder.RequiresQualifier(method, target, + nonVirtualDispatch: expectedTargetDetails.CallOpCode != OpCode.CallVirt); } - targetResolveResult = requireTarget ? target.ResolveResult : null; } else { // HACK: this is a special case for collection initializer calls, they do not allow a target to be // emitted, but we still need it for overload resolution. requireTarget = true; - targetResolveResult = target.ResolveResult; - } - - // initialize requireTypeArguments flag - bool requireTypeArguments; - IType[] typeArguments; - bool appliedRequireTypeArgumentsShortcut = false; - if (method.TypeParameters.Count > 0 && (allowedTransforms & CallTransformation.RequireTypeArguments) != 0 - && !IsPossibleExtensionMethodCallOnNull(method, argumentList.Arguments)) - { - // The ambiguity resolution below only adds type arguments as last resort measure, however there are - // methods, such as Enumerable.OfType(IEnumerable input) that always require type arguments, - // as those cannot be inferred from the parameters, which leads to bloated expressions full of extra casts - // that are no longer required once we add the type arguments. - // We lend overload resolution a hand by detecting such cases beforehand and requiring type arguments, - // if necessary. - if (!CanInferTypeArgumentsFromArguments(method, argumentList, expressionBuilder.typeInference)) - { - if (settings.AnonymousTypes - && method.TypeArguments.Any(a => a.ContainsAnonymousType()) - && PinTypesOfNullArguments(argumentList) - && CanInferTypeArgumentsFromArguments(method, argumentList, expressionBuilder.typeInference)) - { - // Anonymous types cannot be written as explicit type arguments; instead the - // null arguments were rewritten so that all type arguments are inferable. - requireTypeArguments = false; - typeArguments = Empty.Array; - } - else - { - requireTypeArguments = true; - typeArguments = method.TypeArguments.ToArray(); - appliedRequireTypeArgumentsShortcut = true; - } - } - else - { - requireTypeArguments = false; - typeArguments = Empty.Array; - } - } - else - { - requireTypeArguments = false; - typeArguments = Empty.Array; } - bool targetCasted = false; - bool argumentsCasted = false; - bool originalRequireTarget = requireTarget; - bool skipTargetCast = method.Accessibility <= Accessibility.Protected && expressionBuilder.IsBaseTypeOfCurrentType(method.DeclaringTypeDefinition); - OverloadResolutionErrors errors; - while ((errors = IsUnambiguousCall(expectedTargetDetails, method, targetResolveResult, typeArguments, - argumentList.GetArgumentResolveResults().ToArray(), argumentList.GetArgumentNames(), argumentList.FirstOptionalArgumentIndex, out foundMethod, - out var bestCandidateIsExpandedForm)) != OverloadResolutionErrors.None || bestCandidateIsExpandedForm != argumentList.IsExpandedForm) - { - switch (errors) - { - case OverloadResolutionErrors.OutVarTypeMismatch: - Debug.Assert(argumentList.UseImplicitlyTypedOut); - argumentList.UseImplicitlyTypedOut = false; - continue; - case OverloadResolutionErrors.TypeInferenceFailed: - if ((allowedTransforms & CallTransformation.RequireTypeArguments) != 0) - { - goto case OverloadResolutionErrors.WrongNumberOfTypeArguments; - } - goto default; - case OverloadResolutionErrors.WrongNumberOfTypeArguments: - Debug.Assert((allowedTransforms & CallTransformation.RequireTypeArguments) != 0); - if (requireTypeArguments) - goto default; - requireTypeArguments = true; - typeArguments = method.TypeArguments.ToArray(); - continue; - case OverloadResolutionErrors.MissingArgumentForRequiredParameter: - if (argumentList.FirstOptionalArgumentIndex == -1) - goto default; - argumentList.FirstOptionalArgumentIndex = -1; - continue; - default: - // TODO : implement some more intelligent algorithm that decides which of these fixes (cast args, add target, cast target, add type args) - // is best in this case. Additionally we should not cast all arguments at once, but step-by-step try to add only a minimal number of casts. - if (argumentList.AddNamesToPrimitiveValues) - { - argumentList.AddNamesToPrimitiveValues = false; - } - else if (argumentList.FirstOptionalArgumentIndex >= 0) - { - argumentList.FirstOptionalArgumentIndex = -1; - } - else if (!argumentsCasted) - { - // If we added type arguments beforehand, but that didn't make the code any better, - // undo that decision and add casts first. - if (appliedRequireTypeArgumentsShortcut) - { - requireTypeArguments = false; - typeArguments = Empty.Array; - appliedRequireTypeArgumentsShortcut = false; - } - argumentsCasted = true; - argumentList.UseImplicitlyTypedOut = false; - CastArguments(argumentList.Arguments, argumentList.ExpectedParameters); - } - else if ((allowedTransforms & CallTransformation.RequireTarget) != 0 && !requireTarget) - { - requireTarget = true; - targetResolveResult = target.ResolveResult; - } - else if ((allowedTransforms & CallTransformation.RequireTarget) != 0 && !targetCasted) - { - if (skipTargetCast && requireTarget != originalRequireTarget) - { - requireTarget = originalRequireTarget; - if (!originalRequireTarget) - targetResolveResult = null; - allowedTransforms &= ~CallTransformation.RequireTarget; - } - else - { - targetCasted = true; - target = target.ConvertTo(method.DeclaringType, expressionBuilder); - targetResolveResult = target.ResolveResult; - } - } - else if ((allowedTransforms & CallTransformation.RequireTypeArguments) != 0 && !requireTypeArguments) - { - requireTypeArguments = true; - typeArguments = method.TypeArguments.ToArray(); - } - else if ((allowedTransforms & CallTransformation.EnforceExplicitIn) != 0) - { - EnforceExplicitIn(argumentList.Arguments, argumentList.ExpectedParameters); - allowedTransforms &= ~CallTransformation.EnforceExplicitIn; - } - else - { - break; - } - continue; - } - // We've given up. - foundMethod = method; - break; - } - if ((allowedTransforms & CallTransformation.RequireTarget) != 0 && requireTarget) - transform |= CallTransformation.RequireTarget; - if ((allowedTransforms & CallTransformation.RequireTypeArguments) != 0 && requireTypeArguments) - transform |= CallTransformation.RequireTypeArguments; + var disambiguator = Disambiguator.ForCall(expressionBuilder, method, argumentList, target, + requireTarget, expectedTargetDetails, allowedTransforms); + // Where even the most explicit spelling stays ambiguous, the call is written as it + // stands and annotated with the method it was meant to reach. + foundMethod = disambiguator.Resolved ? (IParameterizedMember?)disambiguator.FoundMember : method; + target = disambiguator.Target; + argumentList = disambiguator.Arguments; + requireTarget = disambiguator.RequireTarget; + bool requireTypeArguments = disambiguator.RequireTypeArguments; + + if ((allowedTransforms & ReferenceTransformation.RequireTarget) != 0 && requireTarget) + transform |= ReferenceTransformation.RequireTarget; + if ((allowedTransforms & ReferenceTransformation.RequireTypeArguments) != 0 && requireTypeArguments) + transform |= ReferenceTransformation.RequireTypeArguments; if (argumentList.FirstOptionalArgumentIndex < 0) - transform |= CallTransformation.NoOptionalArgumentAllowed; + transform |= ReferenceTransformation.NoOptionalArgumentAllowed; if (!argumentList.AddNamesToPrimitiveValues) - transform |= CallTransformation.NoNamedArgsForPrettiness; + transform |= ReferenceTransformation.NoNamedArgsForPrettiness; return transform; } - private void EnforceExplicitIn(TranslatedExpression[] arguments, IParameter[] expectedParameters) - { - for (int i = 0; i < arguments.Length; i++) - { - if (expectedParameters[i].ReferenceKind != ReferenceKind.In) - continue; - if (arguments[i].Expression is DirectionExpression) - continue; - - arguments[i] = WrapInAsRefReadOnly(arguments[i]); - expressionBuilder.statementBuilder.EmitAsRefReadOnly = true; - } - } - - private TranslatedExpression WrapInAsRefReadOnly(TranslatedExpression arg) - { - return new DirectionExpression( - FieldDirection.In, - new InvocationExpression { - Target = new IdentifierExpression("ILSpyHelper_AsRefReadOnly"), - Arguments = { arg.Expression } - } - ).WithRR(new ByReferenceResolveResult(arg.Type, ReferenceKind.In)) - .WithoutILInstruction(); - } - - private bool IsPossibleExtensionMethodCallOnNull(IMethod method, IList arguments) - { - return method.IsExtensionMethod && arguments.Count > 0 && arguments[0].Expression is NullReferenceExpression; - } - - static bool CanInferTypeArgumentsFromArguments(IMethod method, ArgumentList argumentList, TypeInference typeInference) - { - if (method.TypeParameters.Count == 0) - return true; - // always use unspecialized member, otherwise type inference fails - method = (IMethod)method.MemberDefinition; - IReadOnlyList paramTypesInArgumentOrder; - if (argumentList.ArgumentToParameterMap == null) - paramTypesInArgumentOrder = method.Parameters.SelectReadOnlyArray(p => p.Type); - else - paramTypesInArgumentOrder = argumentList.ArgumentToParameterMap - .SelectReadOnlyArray( - index => index >= 0 ? method.Parameters[index].Type : SpecialType.UnknownType - ); - typeInference.InferTypeArguments(method.TypeParameters, - argumentList.Arguments.SelectReadOnlyArray(a => a.ResolveResult), paramTypesInArgumentOrder, - out bool success); - return success; - } - - /// - /// C# has no syntax to spell out an anonymous type, so a null literal cannot be given such - /// a type with a cast. The minimal expression that produces a null value of an anonymous - /// type is a conditional expression whose never-taken branch creates an instance of the - /// type: true ? null : new { A = default(int) }. - /// Replaces null-literal arguments of an anonymous type with such an expression, so that - /// type arguments involving anonymous types (which cannot be written explicitly either) - /// become inferable from the arguments. - /// Returns true, if at least one argument was replaced. - /// - private bool PinTypesOfNullArguments(ArgumentList argumentList) - { - bool anyArgumentReplaced = false; - for (int i = 0; i < argumentList.Length; i++) - { - IType expectedType = argumentList.ExpectedParameters[i].Type; - if (argumentList.Arguments[i].Expression is not NullReferenceExpression) - continue; - if (!expectedType.IsAnonymousType() || NewAnonymousTypeInstance(expectedType) is not NewObj newObj) - continue; - var nullLiteral = argumentList.Arguments[i]; - argumentList.Arguments[i] = new ConditionalExpression(new PrimitiveExpression(true), - nullLiteral.Expression.Detach(), expressionBuilder.Translate(newObj, expectedType)) - .WithILInstruction(nullLiteral.ILInstructions) - .WithRR(new ResolveResult(expectedType)); - anyArgumentReplaced = true; - } - return anyArgumentReplaced; - } - - /// - /// Builds a 'newobj' instruction creating an instance of the anonymous type - /// with default property values; translating it yields - /// object-initializer syntax, the only way to name the type in source code. Returns null - /// if a property type involves an anonymous type other than by direct nesting (e.g. an - /// array of anonymous type), because its default value expression would have to name it. - /// - private NewObj? NewAnonymousTypeInstance(IType type) - { - var newObj = new NewObj(type.GetConstructors().Single()); - foreach (var parameter in newObj.Method.Parameters) - { - ILInstruction? argument = parameter.Type.IsAnonymousType() - ? NewAnonymousTypeInstance(parameter.Type) - : parameter.Type.ContainsAnonymousType() ? null : new DefaultValue(parameter.Type); - if (argument == null) - return null; - newObj.Arguments.Add(argument); - } - return newObj; - } - - private void CastArguments(IList arguments, IList expectedParameters) - { - for (int i = 0; i < arguments.Count; i++) - { - if (settings.AnonymousTypes && expectedParameters[i].Type.ContainsAnonymousType()) - { - if (arguments[i].Expression is LambdaExpression lambda) - { - ModifyReturnTypeOfLambda(lambda); - } - } - else - { - IParameter parameter = expectedParameters[i]; - IType parameterType; - if (parameter.Type.Kind == TypeKind.Dynamic) - { - parameterType = expressionBuilder.compilation.FindType(KnownTypeCode.Object); - } - else - { - parameterType = parameter.Type; - } - - if (parameter.ReferenceKind == ReferenceKind.In && parameterType is ByReferenceType brt && arguments[i].Type is not ByReferenceType) - { - parameterType = brt.ElementType; - } - - arguments[i] = arguments[i].ConvertTo(parameterType, expressionBuilder, allowImplicitConversion: false); - } - } - } - static bool IsNullConditional(Expression expr) { return expr is UnaryOperatorExpression uoe && uoe.Operator == UnaryOperatorType.NullConditional; } - private void ModifyReturnTypeOfLambda(LambdaExpression lambda) - { - var resolveResult = (DecompiledLambdaResolveResult)lambda.GetResolveResult(); - if (lambda.Body is Expression exprBody) - lambda.Body = new TranslatedExpression(exprBody.Detach()).ConvertTo(resolveResult.ReturnType, expressionBuilder); - else - ModifyReturnStatementInsideLambda(resolveResult.ReturnType, lambda); - resolveResult.InferredReturnType = resolveResult.ReturnType; - } - - private void ModifyReturnStatementInsideLambda(IType returnType, AstNode parent) - { - foreach (var child in parent.Children) - { - if (child is LambdaExpression || child is AnonymousMethodExpression) - continue; - if (child is ReturnStatement ret) - { - if (ret.Expression is not null) - ret.Expression = new TranslatedExpression(ret.Expression.Detach()).ConvertTo(returnType, expressionBuilder); - continue; - } - ModifyReturnStatementInsideLambda(returnType, child); - } - } - private bool IsDelegateEqualityComparison(IMethod method, IList arguments) { // Comparison on a delegate type is a C# builtin operator @@ -1653,218 +1343,34 @@ private ExpressionWithResolveResult HandleImplicitConversion(IMethod method, Tra .WithRR(new ConversionResolveResult(targetType, argument.ResolveResult, conv)); } - OverloadResolutionErrors IsUnambiguousCall(ExpectedTargetDetails expectedTargetDetails, IMethod method, - ResolveResult? target, IType[] typeArguments, ResolveResult[] arguments, - string[]? argumentNames, int firstOptionalArgumentIndex, - out IParameterizedMember? foundMember, out bool bestCandidateIsExpandedForm) - { - foundMember = null; - bestCandidateIsExpandedForm = false; - var currentTypeDefinition = resolver.CurrentTypeDefinition; - var lookup = new MemberLookup(currentTypeDefinition, currentTypeDefinition.ParentModule); - - Log.WriteLine("IsUnambiguousCall: Performing overload resolution for " + method); - Log.WriteCollection(" Arguments: ", arguments); - - argumentNames = firstOptionalArgumentIndex < 0 || argumentNames == null - ? argumentNames - : argumentNames.Take(firstOptionalArgumentIndex).ToArray(); - - var or = new OverloadResolution(resolver.Compilation, - arguments, argumentNames, typeArguments, - conversions: expressionBuilder.resolver.conversions); - if (expectedTargetDetails.CallOpCode == OpCode.NewObj) - { - foreach (IMethod ctor in method.DeclaringType.GetConstructors()) - { - bool allowProtectedAccess = - resolver.CurrentTypeDefinition == method.DeclaringTypeDefinition; - if (lookup.IsAccessible(ctor, allowProtectedAccess)) - { - Log.Indent(); - OverloadResolutionErrors errors = or.AddCandidate(ctor); - Log.Unindent(); - or.LogCandidateAddingResult(" Candidate", ctor, errors); - } - } - } - else if (method.IsOperator) - { - IEnumerable operatorCandidates; - if (arguments.Length == 1) - { - IType argType = NullableType.GetUnderlyingType(arguments[0].Type); - operatorCandidates = resolver.GetUserDefinedOperatorCandidates(argType, method.Name); - if (method.Name == "op_Explicit") - { - // For casts, also consider candidates from the target type we are casting to. - var hashSet = new HashSet(operatorCandidates); - IType targetType = NullableType.GetUnderlyingType(method.ReturnType); - hashSet.UnionWith( - resolver.GetUserDefinedOperatorCandidates(targetType, method.Name) - ); - operatorCandidates = hashSet; - } - } - else if (arguments.Length == 2) - { - IType lhsType = NullableType.GetUnderlyingType(arguments[0].Type); - IType rhsType = NullableType.GetUnderlyingType(arguments[1].Type); - var hashSet = new HashSet(); - hashSet.UnionWith(resolver.GetUserDefinedOperatorCandidates(lhsType, method.Name)); - hashSet.UnionWith(resolver.GetUserDefinedOperatorCandidates(rhsType, method.Name)); - operatorCandidates = hashSet; - } - else - { - operatorCandidates = EmptyList.Instance; - } - foreach (var m in operatorCandidates) - { - or.AddCandidate(m); - } - } - else if (target == null) - { - var result = resolver.ResolveSimpleName(method.Name, typeArguments, isInvocationTarget: true) - as MethodGroupResolveResult; - if (result == null) - return OverloadResolutionErrors.AmbiguousMatch; - or.AddMethodLists(result.MethodsGroupedByDeclaringType.ToArray()); - } - else - { - var result = lookup.Lookup(target, method.Name, typeArguments, isInvocation: true) as MethodGroupResolveResult; - if (result == null) - return OverloadResolutionErrors.AmbiguousMatch; - or.AddMethodLists(result.MethodsGroupedByDeclaringType.ToArray()); - } - bestCandidateIsExpandedForm = or.BestCandidateIsExpandedForm; - if (or.BestCandidateErrors != OverloadResolutionErrors.None) - return or.BestCandidateErrors; - if (or.IsAmbiguous) - return OverloadResolutionErrors.AmbiguousMatch; - foundMember = or.GetBestCandidateWithSubstitutedTypeArguments(); - if (foundMember == null) - { - // Overload resolution reports no error for an empty candidate set - there is no - // best candidate to carry one - so a call that matched nothing has to be reported - // as unresolvable here. - return OverloadResolutionErrors.AmbiguousMatch; - } - if (!IsAppropriateCallTarget(expectedTargetDetails, method, foundMember)) - return OverloadResolutionErrors.AmbiguousMatch; - var map = or.GetArgumentToParameterMap(); - for (int i = 0; i < arguments.Length; i++) - { - ResolveResult arg = arguments[i]; - int parameterIndex = map[i]; - if (arg is OutVarResolveResult rr && parameterIndex >= 0) - { - var param = foundMember.Parameters[parameterIndex]; - var paramType = param.Type.UnwrapByRef(); - if (!paramType.Equals(rr.OriginalVariableType)) - return OverloadResolutionErrors.OutVarTypeMismatch; - } - } - - return OverloadResolutionErrors.None; - } - - bool IsUnambiguousAccess(ExpectedTargetDetails expectedTargetDetails, ResolveResult? target, IMethod method, - IList arguments, string[]? argumentNames, [NotNullWhen(true)] out IMember? foundMember) - { - Log.WriteLine("IsUnambiguousAccess: Performing overload resolution for " + method); - Log.WriteCollection(" Arguments: ", arguments.Select(a => a.ResolveResult)); - - foundMember = null; - if (target == null) - { - var result = resolver.ResolveSimpleName(method.AccessorOwner!.Name, - EmptyList.Instance, - isInvocationTarget: false) as MemberResolveResult; - if (result == null || result.IsError) - return false; - foundMember = result.Member; - } - else - { - var lookup = new MemberLookup(resolver.CurrentTypeDefinition, resolver.CurrentTypeDefinition.ParentModule); - if (method.AccessorOwner!.SymbolKind == SymbolKind.Indexer) - { - var or = new OverloadResolution(resolver.Compilation, - arguments.SelectArray(a => a.ResolveResult), - argumentNames: argumentNames, - typeArguments: Empty.Array, - conversions: expressionBuilder.resolver.conversions); - or.AddMethodLists(lookup.LookupIndexers(target)); - if (or.BestCandidateErrors != OverloadResolutionErrors.None) - return false; - if (or.IsAmbiguous) - return false; - foundMember = or.GetBestCandidateWithSubstitutedTypeArguments(); - } - else - { - var result = lookup.Lookup(target, - method.AccessorOwner!.Name, - EmptyList.Instance, - isInvocation: false) as MemberResolveResult; - if (result == null || result.IsError) - return false; - foundMember = result.Member; - } - } - return foundMember != null && IsAppropriateCallTarget(expectedTargetDetails, method.AccessorOwner, foundMember); - } - ExpressionWithResolveResult HandleAccessorCall(ExpectedTargetDetails expectedTargetDetails, IMethod method, - TranslatedExpression target, List arguments, string[]? argumentNames) + TranslatedExpression target, ArgumentList argumentList) { - bool requireTarget; - if (settings.AlwaysQualifyMemberReferences || method.AccessorOwner!.SymbolKind == SymbolKind.Indexer || expressionBuilder.HidesVariableWithName(method.AccessorOwner.Name)) - requireTarget = true; - else if (method.IsStatic) - requireTarget = !expressionBuilder.IsCurrentOrContainingType(method.DeclaringTypeDefinition); - else - requireTarget = !(target.Expression is ThisReferenceExpression); - bool targetCasted = false; + // An indexer has no name of its own to write, so it can never drop its target. + bool requireTarget = method.AccessorOwner!.SymbolKind == SymbolKind.Indexer + || expressionBuilder.RequiresQualifier(method.AccessorOwner, target, + nonVirtualDispatch: expectedTargetDetails.CallOpCode != OpCode.CallVirt); bool isSetter = method.ReturnType.IsKnownType(KnownTypeCode.Void); - bool argumentsCasted = (isSetter && method.Parameters.Count == 1) || (!isSetter && method.Parameters.Count == 0); - var targetResolveResult = requireTarget ? target.ResolveResult : null; TranslatedExpression value = default(TranslatedExpression); + var arguments = argumentList.Arguments; if (isSetter) { - value = arguments.Last(); - arguments.Remove(value); + // The assigned value is not part of the reference being spelled out. + value = arguments[arguments.Length - 1]; + arguments = arguments.Take(arguments.Length - 1).ToArray(); } + // The accessor's own parameters describe the indices, which is what the casts and the + // ambiguity check need; the parameters the call was built with describe the call. + argumentList.Arguments = arguments; + argumentList.ExpectedParameters = method.Parameters.ToArray(); - IMember? foundMember; - while (!IsUnambiguousAccess(expectedTargetDetails, targetResolveResult, method, arguments, argumentNames, out foundMember)) - { - if (!argumentsCasted) - { - argumentsCasted = true; - CastArguments(arguments, method.Parameters.ToList()); - } - else if (!requireTarget) - { - requireTarget = true; - targetResolveResult = target.ResolveResult; - } - else if (!targetCasted) - { - targetCasted = true; - target = target.ConvertTo(method.AccessorOwner!.DeclaringType, expressionBuilder); - targetResolveResult = target.ResolveResult; - } - else - { - foundMember = method.AccessorOwner!; - break; - } - } + var disambiguator = Disambiguator.ForAccessor(expressionBuilder, method, argumentList, target, + requireTarget, expectedTargetDetails); + IMember? foundMember = disambiguator.Resolved ? disambiguator.FoundMember : method.AccessorOwner!; + requireTarget = disambiguator.RequireTarget; + target = disambiguator.Target; + arguments = disambiguator.Arguments.Arguments; var rr = new MemberResolveResult(target.ResolveResult, foundMember); @@ -1872,7 +1378,7 @@ ExpressionWithResolveResult HandleAccessorCall(ExpectedTargetDetails expectedTar { TranslatedExpression expr; - if (arguments.Count != 0) + if (arguments.Length != 0) { expr = new IndexerExpression(target.ResolveResult is InitializedObjectResolveResult ? null : target.Expression, arguments.Select(a => a.Expression)) .WithoutILInstruction().WithRR(rr); @@ -1904,7 +1410,7 @@ ExpressionWithResolveResult HandleAccessorCall(ExpectedTargetDetails expectedTar } else { - if (arguments.Count != 0) + if (arguments.Length != 0) { return new IndexerExpression(target.Expression, arguments.Select(a => a.Expression)) .WithoutILInstruction().WithRR(rr); @@ -1922,42 +1428,6 @@ ExpressionWithResolveResult HandleAccessorCall(ExpectedTargetDetails expectedTar } } - bool IsAppropriateCallTarget(ExpectedTargetDetails expectedTargetDetails, IMember expectedTarget, IMember actualTarget) - { - if (expectedTarget.Equals(actualTarget, NormalizeTypeVisitor.TypeErasure)) - return true; - - if (expectedTargetDetails.CallOpCode == OpCode.CallVirt && actualTarget.IsOverride) - { - if (expectedTargetDetails.NeedsBoxingConversion && actualTarget.DeclaringType.IsReferenceType != true) - return false; - foreach (var possibleTarget in InheritanceHelper.GetBaseMembers(actualTarget, false)) - { - if (expectedTarget.Equals(possibleTarget, NormalizeTypeVisitor.TypeErasure)) - return true; - if (!possibleTarget.IsOverride) - break; - } - } - return false; - } - - - /// - /// Checks whether calling `target.methodName()` will use `expected` as the method to invoke. - /// - public bool CheckSimpleCall(ResolveResult target, IMethod expected, OpCode expectedCallOpCode = OpCode.Call) - { - var details = new ExpectedTargetDetails { CallOpCode = expectedCallOpCode, NeedsBoxingConversion = false }; - if (resolver.ResolveMemberAccess(target, expected.Name, [], NameLookupMode.InvocationTarget) - is not MethodGroupResolveResult mgrr) - return false; - var or = mgrr.PerformOverloadResolution(typeSystem, []); - if (or.BestCandidateErrors != OverloadResolutionErrors.None || or.IsAmbiguous) - return false; - return IsAppropriateCallTarget(details, expected, or.GetBestCandidateWithSubstitutedTypeArguments()!); - } - ExpressionWithResolveResult HandleConstructorCall(ExpectedTargetDetails expectedTargetDetails, ResolveResult? target, IMethod method, ArgumentList argumentList) { if (settings.AnonymousTypes && method.DeclaringType.IsAnonymousType()) @@ -1986,24 +1456,9 @@ ExpressionWithResolveResult HandleConstructorCall(ExpectedTargetDetails expected } else { - while (IsUnambiguousCall(expectedTargetDetails, method, null, Empty.Array, - argumentList.GetArgumentResolveResults().ToArray(), - argumentList.GetArgumentNames(), argumentList.FirstOptionalArgumentIndex, out _, - out var bestCandidateIsExpandedForm) != OverloadResolutionErrors.None || bestCandidateIsExpandedForm != argumentList.IsExpandedForm) - { - if (argumentList.AddNamesToPrimitiveValues) - { - argumentList.AddNamesToPrimitiveValues = false; - continue; - } - if (argumentList.FirstOptionalArgumentIndex >= 0) - { - argumentList.FirstOptionalArgumentIndex = -1; - continue; - } - CastArguments(argumentList.Arguments, argumentList.ExpectedParameters); - break; // make sure that we don't not end up in an infinite loop - } + var disambiguator = Disambiguator.ForConstructorCall(expressionBuilder, method, argumentList, + expectedTargetDetails); + argumentList = disambiguator.Arguments; IType? returnTypeOverride = null; if (typeSystem.MainModule.TypeSystemOptions.HasFlag(TypeSystemOptions.NativeIntegersWithoutAttribute)) { @@ -2154,40 +1609,11 @@ ExpressionWithResolveResult BuildDelegateReference(IMethod method, IMethod? invo thisArg = thisArgBox.Argument; } TranslatedExpression target = expressionBuilder.Translate(thisArg!, targetType); - var currentTarget = target; - bool targetCasted = false; - bool addTypeArguments = false; - // Initial inputs for IsUnambiguousMethodReference: - ResolveResult targetResolveResult = target.ResolveResult; - IReadOnlyList typeArguments = EmptyList.Instance; - if (thisArg!.MatchLdNull()) - { - targetCasted = true; - currentTarget = currentTarget.ConvertTo(targetType, expressionBuilder); - targetResolveResult = currentTarget.ResolveResult; - } - // Find somewhat minimal solution: - ResolveResult? result; - while (!IsUnambiguousMethodReference(expectedTargetDetails, method, targetResolveResult, typeArguments, true, out result)) - { - if (!targetCasted) - { - // try casting target - targetCasted = true; - currentTarget = currentTarget.ConvertTo(targetType, expressionBuilder); - targetResolveResult = currentTarget.ResolveResult; - continue; - } - if (!addTypeArguments) - { - // try adding type arguments - addTypeArguments = true; - typeArguments = method.TypeArguments; - continue; - } - break; - } - return (currentTarget, addTypeArguments, method.Name, result!); + // A null literal carries no type at all, so its cast cannot wait its turn. + var disambiguator = Disambiguator.ForMethodReference(expressionBuilder, method, targetType, + target, requireTarget: true, expectedTargetDetails, isExtensionMethodReference: true, + castTargetUpFront: thisArg!.MatchLdNull()); + return (disambiguator.Target, disambiguator.RequireTypeArguments, method.Name, disambiguator.Result!); } else { @@ -2211,52 +1637,19 @@ ExpressionWithResolveResult BuildDelegateReference(IMethod method, IMethod? invo memberStatic: method.IsStatic, memberDeclaringType: method.DeclaringType); // check if target is required - bool requireTarget = expressionBuilder.HidesVariableWithName(method.Name) - || (method.IsStatic ? !expressionBuilder.IsCurrentOrContainingType(method.DeclaringTypeDefinition) : !(target.Expression is ThisReferenceExpression)); - // Try to find minimal expression - // If target is required, include it from the start - bool targetAdded = requireTarget; - TranslatedExpression currentTarget = targetAdded ? target : default; - // Remember other decisions: - bool targetCasted = false; - bool addTypeArguments = false; - // Initial inputs for IsUnambiguousMethodReference: - ResolveResult? targetResolveResult = targetAdded ? target.ResolveResult : null; - IReadOnlyList typeArguments = EmptyList.Instance; - // Find somewhat minimal solution: - ResolveResult? result; - while (!IsUnambiguousMethodReference(expectedTargetDetails, method, targetResolveResult, typeArguments, false, out result)) - { - if (!addTypeArguments) - { - // try adding type arguments - addTypeArguments = true; - typeArguments = method.TypeArguments; - continue; - } - if (!targetAdded) - { - // try adding target - targetAdded = true; - currentTarget = target; - targetResolveResult = target.ResolveResult; - continue; - } - if (!targetCasted) - { - // try casting target - targetCasted = true; - currentTarget = currentTarget.ConvertTo(targetType, expressionBuilder); - targetResolveResult = currentTarget.ResolveResult; - continue; - } - break; - } + bool requireTarget = expressionBuilder.RequiresQualifier(method, target, + nonVirtualDispatch: expectedTargetDetails.CallOpCode != OpCode.CallVirt); + var disambiguator = Disambiguator.ForMethodReference(expressionBuilder, method, targetType, + target, requireTarget, expectedTargetDetails, isExtensionMethodReference: false); + ResolveResult? result = disambiguator.Result; if (result is MethodGroupResolveResult mgrr) { result = mgrr.WithChosenMethod(method); } - return (currentTarget, addTypeArguments, method.Name, result!); + // BuildDelegateReference tells a qualified reference from an unqualified one by + // whether it got a target expression at all. + return (disambiguator.RequireTarget ? disambiguator.Target : default, + disambiguator.RequireTypeArguments, method.Name, result!); } } @@ -2273,54 +1666,6 @@ TranslatedExpression HandleDelegateConstruction(IType delegateType, IMethod meth return oce; } - bool IsUnambiguousMethodReference(ExpectedTargetDetails expectedTargetDetails, IMethod method, ResolveResult? target, IReadOnlyList typeArguments, bool isExtensionMethodReference, [NotNullWhen(true)] out ResolveResult? result) - { - Log.WriteLine("IsUnambiguousMethodReference: Performing overload resolution for " + method); - - var lookup = new MemberLookup(resolver.CurrentTypeDefinition, resolver.CurrentTypeDefinition.ParentModule); - OverloadResolution or; - - if (isExtensionMethodReference) - { - result = resolver.ResolveMemberAccess(target, method.Name, typeArguments, NameLookupMode.InvocationTarget) as MethodGroupResolveResult; - if (result == null) - return false; - or = ((MethodGroupResolveResult)result).PerformOverloadResolution(resolver.CurrentTypeResolveContext.Compilation, - method.Parameters.SelectReadOnlyArray(p => new TypeResolveResult(p.Type)), - argumentNames: null, allowExtensionMethods: true); - if (or == null || or.IsAmbiguous) - return false; - } - else - { - or = new OverloadResolution(resolver.Compilation, - arguments: method.Parameters.SelectReadOnlyArray(p => new TypeResolveResult(p.Type)), // there are no arguments, use parameter types - argumentNames: null, // argument names are not possible - typeArguments.ToArray(), - conversions: expressionBuilder.resolver.conversions - ); - if (target == null) - { - result = resolver.ResolveSimpleName(method.Name, typeArguments, isInvocationTarget: false); - if (!(result is MethodGroupResolveResult mgrr)) - return false; - or.AddMethodLists(mgrr.MethodsGroupedByDeclaringType.ToArray()); - } - else - { - result = lookup.Lookup(target, method.Name, typeArguments, isInvocation: false); - if (!(result is MethodGroupResolveResult mgrr)) - return false; - or.AddMethodLists(mgrr.MethodsGroupedByDeclaringType.ToArray()); - } - } - - var foundMethod = or.GetBestCandidateWithSubstitutedTypeArguments(); - if (!IsAppropriateCallTarget(expectedTargetDetails, method, foundMethod)) - return false; - return result is MethodGroupResolveResult; - } - static MethodGroupResolveResult ToMethodGroup(IMethod method, ILFunction localFunction) { return new MethodGroupResolveResult( diff --git a/ICSharpCode.Decompiler/CSharp/Disambiguator.cs b/ICSharpCode.Decompiler/CSharp/Disambiguator.cs new file mode 100644 index 00000000000..6ca7dff76cc --- /dev/null +++ b/ICSharpCode.Decompiler/CSharp/Disambiguator.cs @@ -0,0 +1,995 @@ +// Copyright (c) 2026 Siegfried Pammer +// +// 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. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +using ICSharpCode.Decompiler.CSharp.Resolver; +using ICSharpCode.Decompiler.CSharp.Syntax; +using ICSharpCode.Decompiler.IL; +using ICSharpCode.Decompiler.Semantics; +using ICSharpCode.Decompiler.TypeSystem; +using ICSharpCode.Decompiler.Util; + +#nullable enable + +namespace ICSharpCode.Decompiler.CSharp +{ + /// + /// A way of making a member reference more explicit, so that it resolves back to the + /// member the IL referenced instead of to something the shorter spelling happens to hit. + /// + [Flags] + internal enum ReferenceTransformation + { + None = 0, + RequireTarget = 1, + RequireTypeArguments = 2, + NoOptionalArgumentAllowed = 4, + /// + /// Add calls to AsRefReadOnly for in parameters that did not have an explicit DirectionExpression yet. + /// + EnforceExplicitIn = 8, + NoNamedArgsForPrettiness = 0x10, + /// Cast every argument to its parameter type. + CastArguments = 0x20, + /// Cast the target to the declaring type. + CastTarget = 0x40, + All = 0x7f, + } + + /// + /// Begins escalating a field access. A field is only ever qualified and then cast; it has + /// no arguments and no type arguments to make explicit. + /// + + /// + /// Finds the shortest spelling of a member reference that still resolves back to the member + /// the IL referenced. A short spelling may bind to something else in the output's + /// name-lookup context, so it is re-resolved and, while it does not bind back, made more + /// explicit and tried again. Which check re-resolves it, and which escalations are open to it + /// in what order, are both settled when it is built. + /// + /// One of the For* factories builds it, escalates, and hands it back finished; the results + /// are the fields below, not a return value. + /// + /// In: the member, its declaring type, and how much room the surrounding syntax leaves for + /// escalations. + /// + /// In and out: , and + /// are seeded with the shortest spelling and escalated in place. + /// + /// Out: and say whether the + /// target and the type arguments are written at all; and + /// are what the last check resolved to. is + /// false when no spelling resolved, leaving the most explicit one tried for the caller to + /// emit anyway. + /// + internal struct Disambiguator + { + + static readonly ReferenceTransformation[] FieldSteps = { + ReferenceTransformation.RequireTarget, + ReferenceTransformation.CastTarget, + }; + + /// A field access: only ever qualified and then cast. + internal static Disambiguator ForField(ExpressionBuilder expressionBuilder, IField field, + TranslatedExpression target, bool requireTarget) + { + var disambiguator = new Disambiguator(expressionBuilder, field, field.DeclaringType, target, + requireTarget, FieldSteps, ReferenceTransformation.All); + disambiguator.Resolved = disambiguator.Run(); + return disambiguator; + } + + static readonly ReferenceTransformation[] MethodReferenceSteps = { + ReferenceTransformation.RequireTypeArguments, + ReferenceTransformation.RequireTarget, + ReferenceTransformation.CastTarget, + }; + + /// + /// An extension method reference always carries its target, so the only escalations + /// left are making that target's type and then the type arguments explicit. + /// + static readonly ReferenceTransformation[] ExtensionMethodReferenceSteps = { + ReferenceTransformation.CastTarget, + ReferenceTransformation.RequireTypeArguments, + }; + + /// + /// A method group. It has no arguments to cast, but unlike a field it can spell out its + /// type arguments. is for a target that carries no + /// type of its own, such as a null literal, where the cast cannot wait its turn. + /// + internal static Disambiguator ForMethodReference(ExpressionBuilder expressionBuilder, IMethod method, + IType targetType, TranslatedExpression target, bool requireTarget, + ExpectedTargetDetails expectedTargetDetails, bool isExtensionMethodReference, + bool castTargetUpFront = false) + { + var disambiguator = new Disambiguator(expressionBuilder, method, targetType, target, requireTarget, + isExtensionMethodReference ? ExtensionMethodReferenceSteps : MethodReferenceSteps, + ReferenceTransformation.All, expectedTargetDetails, + isExtensionMethodReference: isExtensionMethodReference); + if (castTargetUpFront) + { + disambiguator.ApplyUpFront(ReferenceTransformation.CastTarget); + } + disambiguator.Resolved = disambiguator.Run(); + return disambiguator; + } + + static readonly ReferenceTransformation[] CallSteps = { + ReferenceTransformation.NoNamedArgsForPrettiness, + ReferenceTransformation.NoOptionalArgumentAllowed, + ReferenceTransformation.CastArguments, + ReferenceTransformation.RequireTarget, + ReferenceTransformation.CastTarget, + ReferenceTransformation.RequireTypeArguments, + ReferenceTransformation.EnforceExplicitIn, + }; + + /// + /// An ordinary method call - the one reference that can reach for every escalation there + /// is. withholds the ones the syntax around the call has no + /// room for. + /// + internal static Disambiguator ForCall(ExpressionBuilder expressionBuilder, IMethod method, + ArgumentList argumentList, TranslatedExpression target, bool requireTarget, + ExpectedTargetDetails expectedTargetDetails, ReferenceTransformation allowed) + { + var disambiguator = new Disambiguator(expressionBuilder, method, method.DeclaringType, target, + requireTarget, CallSteps, allowed, expectedTargetDetails, invocationSyntax: true, + castingArgumentsDropsImplicitlyTypedOut: true, + skipTargetCast: method.Accessibility <= Accessibility.Protected + && expressionBuilder.IsBaseTypeOfCurrentType(method.DeclaringTypeDefinition)) { + Arguments = argumentList, + }; + disambiguator.WriteTypeArgumentsInferenceCannotReach(); + disambiguator.Resolved = disambiguator.Run(); + return disambiguator; + } + + static readonly ReferenceTransformation[] ConstructorCallSteps = { + ReferenceTransformation.NoNamedArgsForPrettiness, + ReferenceTransformation.NoOptionalArgumentAllowed, + ReferenceTransformation.CastArguments, + }; + + /// + /// A constructor call. There is no target to qualify or cast and the type arguments come + /// from the type being constructed, so only the arguments are left. + /// + internal static Disambiguator ForConstructorCall(ExpressionBuilder expressionBuilder, IMethod method, + ArgumentList argumentList, ExpectedTargetDetails expectedTargetDetails) + { + var disambiguator = new Disambiguator(expressionBuilder, method, method.DeclaringType, default, + requireTarget: false, ConstructorCallSteps, ReferenceTransformation.All, + expectedTargetDetails, invocationSyntax: true) { + Arguments = argumentList, + }; + disambiguator.Resolved = disambiguator.Run(); + return disambiguator; + } + + static readonly ReferenceTransformation[] AccessorSteps = { + ReferenceTransformation.CastArguments, + ReferenceTransformation.RequireTarget, + ReferenceTransformation.CastTarget, + }; + + /// + /// A property, indexer or event access. holds the indices, + /// with the parameters the accessor declares: a setter's value is not part of the + /// reference and must already have been split off. + /// + internal static Disambiguator ForAccessor(ExpressionBuilder expressionBuilder, IMethod accessor, + ArgumentList argumentList, TranslatedExpression target, bool requireTarget, + ExpectedTargetDetails expectedTargetDetails) + { + var disambiguator = new Disambiguator(expressionBuilder, accessor, + accessor.AccessorOwner!.DeclaringType, target, requireTarget, AccessorSteps, + ReferenceTransformation.All, expectedTargetDetails) { + Arguments = argumentList, + }; + if (argumentList.Length == 0) + { + // Nothing to cast: a property access, or an indexer assignment whose value has + // already been split off. + disambiguator.MarkApplied(ReferenceTransformation.CastArguments); + } + disambiguator.Resolved = disambiguator.Run(); + return disambiguator; + } + + // In. Fixed for the life of the escalation. + + readonly ExpressionBuilder expressionBuilder; + readonly IMember member; + readonly IType declaringType; + /// The escalations open to this reference, in the order it prefers them. + readonly ReferenceTransformation[] steps; + /// + /// Whether the reference is written with invocation syntax. Nothing on an IMethod says + /// that: a method group is not, a property access is not even though it is a call in IL, + /// and a parameterized property's accessor is, despite being an accessor. + /// + readonly bool invocationSyntax; + readonly bool isExtensionMethodReference; + /// The escalations this caller has room for; the rest are never offered. + readonly ReferenceTransformation allowed; + readonly ExpectedTargetDetails expectedTargetDetails; + /// + /// Whether casting the arguments also means giving up "out var". It does for a call, + /// whose out arguments are written from the argument list; a constructor call keeps + /// the sugar, and an accessor has no out parameters to write. + /// + readonly bool castingArgumentsDropsImplicitlyTypedOut; + /// + /// Whether a protected member declared in a base type lets the reference drop its + /// qualifier again rather than cast it, which would not compile. + /// + readonly bool skipTargetCast; + /// Whether the reference was qualified before any escalation. + readonly bool initiallyRequiredTarget; + + // In and out. The spelling: seeded short, escalated in place, read back after Run. + + public TranslatedExpression Target; + /// The type arguments to write out; empty while they stay inferred. + public IType[] TypeArguments; + /// The arguments, and the decisions about how to write them. + public ArgumentList Arguments; + /// The escalations that are currently in effect. + ReferenceTransformation Applied; + /// + /// The escalations already offered. Normally the same as , but an + /// escalation that was tried and then taken back again stays spent, so it is not + /// offered again. + /// + ReferenceTransformation spent; + /// + /// Whether the type arguments were written up front because they cannot be inferred. + /// Casting the arguments takes that guess back, so that casts are tried first. + /// + bool TypeArgumentsWereAppliedUpFront; + + // Out. Read back after Run. + + /// Whether the reference is written with its target. + public bool RequireTarget => (Applied & ReferenceTransformation.RequireTarget) != 0; + /// Whether the type arguments are written out rather than left to inference. + public bool RequireTypeArguments => (Applied & ReferenceTransformation.RequireTypeArguments) != 0; + /// What the last check resolved to. + public ResolveResult? Result; + /// The member the last check bound to. + public IMember? FoundMember; + /// Whether a spelling was found that resolves back to the member. + public bool Resolved; + + Disambiguator(ExpressionBuilder expressionBuilder, IMember member, IType declaringType, + TranslatedExpression target, bool requireTarget, + ReferenceTransformation[] steps, ReferenceTransformation allowed, + ExpectedTargetDetails expectedTargetDetails = default, bool invocationSyntax = false, + bool isExtensionMethodReference = false, + bool castingArgumentsDropsImplicitlyTypedOut = false, bool skipTargetCast = false) + { + this.expressionBuilder = expressionBuilder; + this.member = member; + this.declaringType = declaringType; + this.allowed = allowed; + this.Target = target; + this.Applied = requireTarget ? ReferenceTransformation.RequireTarget : ReferenceTransformation.None; + this.spent = this.Applied; + this.TypeArguments = Empty.Array; + this.Result = null; + this.expectedTargetDetails = expectedTargetDetails; + this.steps = steps; + this.invocationSyntax = invocationSyntax; + this.isExtensionMethodReference = isExtensionMethodReference; + this.castingArgumentsDropsImplicitlyTypedOut = castingArgumentsDropsImplicitlyTypedOut; + this.skipTargetCast = skipTargetCast; + this.initiallyRequiredTarget = requireTarget; + this.TypeArgumentsWereAppliedUpFront = false; + this.Arguments = default; + this.FoundMember = null; + this.Resolved = false; + } + + CSharpResolver resolver => expressionBuilder.resolver; + + static bool IsPossibleExtensionMethodCallOnNull(IMethod method, IList arguments) + { + return method.IsExtensionMethod && arguments.Count > 0 && arguments[0].Expression is NullReferenceExpression; + } + + bool CanInferTypeArgumentsFromArguments(IMethod method) + { + if (method.TypeParameters.Count == 0) + return true; + // always use unspecialized member, otherwise type inference fails + method = (IMethod)method.MemberDefinition; + IReadOnlyList paramTypesInArgumentOrder; + if (Arguments.ArgumentToParameterMap == null) + paramTypesInArgumentOrder = method.Parameters.SelectReadOnlyArray(p => p.Type); + else + paramTypesInArgumentOrder = Arguments.ArgumentToParameterMap + .SelectReadOnlyArray( + index => index >= 0 ? method.Parameters[index].Type : SpecialType.UnknownType + ); + expressionBuilder.typeInference.InferTypeArguments(method.TypeParameters, + Arguments.Arguments.SelectReadOnlyArray(a => a.ResolveResult), paramTypesInArgumentOrder, + out bool success); + return success; + } + + /// + /// C# has no syntax to spell out an anonymous type, so a null literal cannot be given such + /// a type with a cast. The minimal expression that produces a null value of an anonymous + /// type is a conditional expression whose never-taken branch creates an instance of the + /// type: true ? null : new { A = default(int) }. + /// Replaces null-literal arguments of an anonymous type with such an expression, so that + /// type arguments involving anonymous types (which cannot be written explicitly either) + /// become inferable from the arguments. + /// Returns true, if at least one argument was replaced. + /// + bool PinTypesOfNullArguments() + { + bool anyArgumentReplaced = false; + for (int i = 0; i < Arguments.Length; i++) + { + IType expectedType = Arguments.ExpectedParameters[i].Type; + if (Arguments.Arguments[i].Expression is not NullReferenceExpression) + continue; + if (!expectedType.IsAnonymousType() || NewAnonymousTypeInstance(expectedType) is not NewObj newObj) + continue; + var nullLiteral = Arguments.Arguments[i]; + Arguments.Arguments[i] = new ConditionalExpression(new PrimitiveExpression(true), + nullLiteral.Expression.Detach(), expressionBuilder.Translate(newObj, expectedType)) + .WithILInstruction(nullLiteral.ILInstructions) + .WithRR(new ResolveResult(expectedType)); + anyArgumentReplaced = true; + } + return anyArgumentReplaced; + } + + /// + /// Builds a 'newobj' instruction creating an instance of the anonymous type + /// with default property values; translating it yields + /// object-initializer syntax, the only way to name the type in source code. Returns null + /// if a property type involves an anonymous type other than by direct nesting (e.g. an + /// array of anonymous type), because its default value expression would have to name it. + /// + NewObj? NewAnonymousTypeInstance(IType type) + { + var newObj = new NewObj(type.GetConstructors().Single()); + foreach (var parameter in newObj.Method.Parameters) + { + ILInstruction? argument = parameter.Type.IsAnonymousType() + ? NewAnonymousTypeInstance(parameter.Type) + : parameter.Type.ContainsAnonymousType() ? null : new DefaultValue(parameter.Type); + if (argument == null) + return null; + newObj.Arguments.Add(argument); + } + return newObj; + } + /// + /// The steps only reach for type arguments as a last resort, but a method such as + /// Enumerable.OfType<TResult>(IEnumerable) can never infer them from its arguments, + /// and waiting leaves the expression full of casts that writing them would have made + /// unnecessary. So they are written up front where inference cannot succeed. Casting + /// the arguments takes that guess back, so casts still come first where they do help. + /// + void WriteTypeArgumentsInferenceCannotReach() + { + if (member is not IMethod method || method.TypeParameters.Count == 0) + return; + if ((allowed & ReferenceTransformation.RequireTypeArguments) == 0) + return; + if (IsPossibleExtensionMethodCallOnNull(method, Arguments.Arguments)) + return; + if (CanInferTypeArgumentsFromArguments(method)) + return; + if (expressionBuilder.settings.AnonymousTypes + && method.TypeArguments.Any(a => a.ContainsAnonymousType()) + && PinTypesOfNullArguments() + && CanInferTypeArgumentsFromArguments(method)) + { + // Anonymous types cannot be written as explicit type arguments; instead the + // null arguments were rewritten so that all type arguments are inferable. + return; + } + ApplyUpFront(ReferenceTransformation.RequireTypeArguments); + TypeArgumentsWereAppliedUpFront = true; + } + + /// + /// Performs an escalation before the first lookup rather than waiting for its turn, for a case the caller already knows the short spelling cannot serve. + /// Spends it either way. + /// + void ApplyUpFront(ReferenceTransformation step) + { + TryApply(step); + } + + /// + /// Escalates until the reference is unambiguous. Returns false once every escalation + /// is spent and it still is not, leaving the caller to emit its best effort. + /// Every escalation is spent by one use, which is what ends the loop. + /// + bool Run() + { + OverloadResolutionErrors errors; + while ((errors = Probe()) != OverloadResolutionErrors.None) + { + if (!TryRepair(errors) && !Escalate()) + return false; + } + return true; + } + + /// + /// Answers one specific overload resolution failure with the escalation that addresses + /// it, instead of taking the steps from the top. Returns false to fall back to the + /// steps, either because the failure has no targeted answer or because the answer is + /// already spent. + /// + bool TryRepair(OverloadResolutionErrors errors) + { + switch (errors) + { + case OverloadResolutionErrors.OutVarTypeMismatch: + Debug.Assert(Arguments.UseImplicitlyTypedOut); + Arguments.UseImplicitlyTypedOut = false; + return true; + case OverloadResolutionErrors.TypeInferenceFailed: + case OverloadResolutionErrors.WrongNumberOfTypeArguments: + return TryApply(ReferenceTransformation.RequireTypeArguments); + case OverloadResolutionErrors.MissingArgumentForRequiredParameter: + return TryApply(ReferenceTransformation.NoOptionalArgumentAllowed); + default: + return false; + } + } + + /// + /// The target as the checks below must see it: null while the reference is still + /// unqualified. Not the target to annotate a result with - an unqualified reference is + /// still translated against . + /// + ResolveResult? LookupTarget => RequireTarget ? Target.ResolveResult : null; + + /// + /// Re-resolves the current spelling. None means it binds back to . + /// + OverloadResolutionErrors Probe() + { + switch (member) + { + case IField field: + return ProbeField(field); + // Invocation syntax first: a parameterized property's accessor is written as a + // call, so IsAccessor must not claim it. + case IMethod method when invocationSyntax: + return ProbeCall(method); + case IMethod { IsAccessor: true } accessor: + // The check can resolve a member and still reject it, so the result is + // what it returns, not whether it found something. + bool unambiguous = IsUnambiguousAccess(expectedTargetDetails, + LookupTarget, accessor, Arguments.Arguments, Arguments.ArgumentNames, + out var foundAccessorOwner); + FoundMember = foundAccessorOwner; + return unambiguous + ? OverloadResolutionErrors.None + : OverloadResolutionErrors.AmbiguousMatch; + case IMethod method: + return IsUnambiguousMethodReference(expectedTargetDetails, method, LookupTarget, + TypeArguments, isExtensionMethodReference, out Result) + ? OverloadResolutionErrors.None + : OverloadResolutionErrors.AmbiguousMatch; + default: + throw new NotSupportedException(member.SymbolKind.ToString()); + } + } + + OverloadResolutionErrors ProbeField(IField field) + { + MemberResolveResult? result; + if (LookupTarget == null) + { + result = resolver.ResolveSimpleName(field.Name, EmptyList.Instance, + isInvocationTarget: false) as MemberResolveResult; + } + else + { + var lookup = CreateLookup(resolver); + result = lookup.Lookup(Target.ResolveResult, field.Name, EmptyList.Instance, + isInvocation: false) as MemberResolveResult; + } + Result = result; + if (result == null || result.IsError || !result.Member.Equals(field, NormalizeTypeVisitor.TypeErasure)) + return OverloadResolutionErrors.AmbiguousMatch; + return OverloadResolutionErrors.None; + } + + OverloadResolutionErrors ProbeCall(IMethod method) + { + var errors = IsUnambiguousCall(expressionBuilder, expectedTargetDetails, method, LookupTarget, + TypeArguments, Arguments.GetArgumentResolveResults().ToArray(), + Arguments.GetArgumentNames(), Arguments.FirstOptionalArgumentIndex, + out var foundMember, out bool bestCandidateIsExpandedForm); + FoundMember = foundMember; + if (errors != OverloadResolutionErrors.None) + return errors; + // Resolving to the same method in the other of its normal and expanded form still + // means the spelling is wrong, and no single error describes that. + return bestCandidateIsExpandedForm != Arguments.IsExpandedForm + ? OverloadResolutionErrors.AmbiguousMatch + : OverloadResolutionErrors.None; + } + + /// + /// Records an escalation the caller performed itself, or one that has nothing to do + /// here, so that it will not be offered. + /// + void MarkApplied(ReferenceTransformation step) + { + Applied |= step; + spent |= step; + } + + /// + /// Performs the first escalation still available, in the order this form of reference + /// prefers them. + /// + bool Escalate() + { + // TODO : implement some more intelligent algorithm that decides which of these fixes (cast args, add target, cast target, add type args) + // is best in this case. Additionally we should not cast all arguments at once, but step-by-step try to add only a minimal number of casts. + foreach (ReferenceTransformation step in steps) + { + if (TryApply(step)) + return true; + } + return false; + } + + /// + /// Performs one escalation, unless it is already spent, forbidden by the caller, or + /// does not apply to this kind of reference. + /// + bool TryApply(ReferenceTransformation step) + { + if ((spent & step) != 0 || (allowed & step) == 0 || !Apply(step)) + return false; + Applied |= step; + spent |= step; + return true; + } + + static MemberLookup CreateLookup(CSharpResolver resolver) + { + return new MemberLookup(resolver.CurrentTypeDefinition, resolver.CurrentTypeDefinition.ParentModule); + } + + /// + /// Overload resolution as the ambiguity checks need it: over the compilation the + /// reference is written in, and with its conversions, which decide whether an argument + /// fits a parameter at all. + /// + static OverloadResolution CreateOverloadResolution(CSharpResolver resolver, + IReadOnlyList arguments, string[]? argumentNames, IType[] typeArguments) + { + return new OverloadResolution(resolver.Compilation, arguments.ToArray(), argumentNames, + typeArguments, conversions: resolver.conversions); + } + + internal static OverloadResolutionErrors IsUnambiguousCall(ExpressionBuilder expressionBuilder, + ExpectedTargetDetails expectedTargetDetails, IMethod method, + ResolveResult? target, IType[] typeArguments, ResolveResult[] arguments, + string[]? argumentNames, int firstOptionalArgumentIndex, + out IParameterizedMember? foundMember, out bool bestCandidateIsExpandedForm) + { + CSharpResolver resolver = expressionBuilder.resolver; + foundMember = null; + bestCandidateIsExpandedForm = false; + var lookup = CreateLookup(resolver); + + Log.WriteLine("IsUnambiguousCall: Performing overload resolution for " + method); + Log.WriteCollection(" Arguments: ", arguments); + + argumentNames = firstOptionalArgumentIndex < 0 || argumentNames == null + ? argumentNames + : argumentNames.Take(firstOptionalArgumentIndex).ToArray(); + + var or = CreateOverloadResolution(resolver, arguments, argumentNames, typeArguments); + if (expectedTargetDetails.CallOpCode == OpCode.NewObj) + { + foreach (IMethod ctor in method.DeclaringType.GetConstructors()) + { + bool allowProtectedAccess = + resolver.CurrentTypeDefinition == method.DeclaringTypeDefinition; + if (lookup.IsAccessible(ctor, allowProtectedAccess)) + { + Log.Indent(); + OverloadResolutionErrors errors = or.AddCandidate(ctor); + Log.Unindent(); + or.LogCandidateAddingResult(" Candidate", ctor, errors); + } + } + } + else if (method.IsOperator) + { + IEnumerable operatorCandidates; + if (arguments.Length == 1) + { + IType argType = NullableType.GetUnderlyingType(arguments[0].Type); + operatorCandidates = resolver.GetUserDefinedOperatorCandidates(argType, method.Name); + if (method.Name == "op_Explicit") + { + // For casts, also consider candidates from the target type we are casting to. + var hashSet = new HashSet(operatorCandidates); + IType targetType = NullableType.GetUnderlyingType(method.ReturnType); + hashSet.UnionWith( + resolver.GetUserDefinedOperatorCandidates(targetType, method.Name) + ); + operatorCandidates = hashSet; + } + } + else if (arguments.Length == 2) + { + IType lhsType = NullableType.GetUnderlyingType(arguments[0].Type); + IType rhsType = NullableType.GetUnderlyingType(arguments[1].Type); + var hashSet = new HashSet(); + hashSet.UnionWith(resolver.GetUserDefinedOperatorCandidates(lhsType, method.Name)); + hashSet.UnionWith(resolver.GetUserDefinedOperatorCandidates(rhsType, method.Name)); + operatorCandidates = hashSet; + } + else + { + operatorCandidates = EmptyList.Instance; + } + foreach (var m in operatorCandidates) + { + or.AddCandidate(m); + } + } + else if (target == null) + { + var result = resolver.ResolveSimpleName(method.Name, typeArguments, isInvocationTarget: true) + as MethodGroupResolveResult; + if (result == null) + return OverloadResolutionErrors.AmbiguousMatch; + or.AddMethodLists(result.MethodsGroupedByDeclaringType.ToArray()); + } + else + { + var result = lookup.Lookup(target, method.Name, typeArguments, isInvocation: true) as MethodGroupResolveResult; + if (result == null) + return OverloadResolutionErrors.AmbiguousMatch; + or.AddMethodLists(result.MethodsGroupedByDeclaringType.ToArray()); + } + bestCandidateIsExpandedForm = or.BestCandidateIsExpandedForm; + if (or.BestCandidateErrors != OverloadResolutionErrors.None) + return or.BestCandidateErrors; + if (or.IsAmbiguous) + return OverloadResolutionErrors.AmbiguousMatch; + foundMember = or.GetBestCandidateWithSubstitutedTypeArguments(); + if (foundMember == null) + { + // Overload resolution reports no error for an empty candidate set - there is no + // best candidate to carry one - so a call that matched nothing has to be reported + // as unresolvable here. + return OverloadResolutionErrors.AmbiguousMatch; + } + if (!IsAppropriateCallTarget(expectedTargetDetails, method, foundMember)) + return OverloadResolutionErrors.AmbiguousMatch; + var map = or.GetArgumentToParameterMap(); + for (int i = 0; i < arguments.Length; i++) + { + ResolveResult arg = arguments[i]; + int parameterIndex = map[i]; + if (arg is OutVarResolveResult rr && parameterIndex >= 0) + { + var param = foundMember.Parameters[parameterIndex]; + var paramType = param.Type.UnwrapByRef(); + if (!paramType.Equals(rr.OriginalVariableType)) + return OverloadResolutionErrors.OutVarTypeMismatch; + } + } + + return OverloadResolutionErrors.None; + } + + bool IsUnambiguousAccess(ExpectedTargetDetails expectedTargetDetails, ResolveResult? target, IMethod method, + IList arguments, string[]? argumentNames, [NotNullWhen(true)] out IMember? foundMember) + { + Log.WriteLine("IsUnambiguousAccess: Performing overload resolution for " + method); + Log.WriteCollection(" Arguments: ", arguments.Select(a => a.ResolveResult)); + + foundMember = null; + if (target == null) + { + var result = resolver.ResolveSimpleName(method.AccessorOwner!.Name, + EmptyList.Instance, + isInvocationTarget: false) as MemberResolveResult; + if (result == null || result.IsError) + return false; + foundMember = result.Member; + } + else + { + var lookup = CreateLookup(resolver); + if (method.AccessorOwner!.SymbolKind == SymbolKind.Indexer) + { + var or = CreateOverloadResolution(resolver, arguments.SelectArray(a => a.ResolveResult), + argumentNames, Empty.Array); + or.AddMethodLists(lookup.LookupIndexers(target)); + if (or.BestCandidateErrors != OverloadResolutionErrors.None) + return false; + if (or.IsAmbiguous) + return false; + foundMember = or.GetBestCandidateWithSubstitutedTypeArguments(); + } + else + { + var result = lookup.Lookup(target, + method.AccessorOwner!.Name, + EmptyList.Instance, + isInvocation: false) as MemberResolveResult; + if (result == null || result.IsError) + return false; + foundMember = result.Member; + } + } + return foundMember != null && IsAppropriateCallTarget(expectedTargetDetails, method.AccessorOwner, foundMember); + } + + bool IsUnambiguousMethodReference(ExpectedTargetDetails expectedTargetDetails, IMethod method, ResolveResult? target, IReadOnlyList typeArguments, bool isExtensionMethodReference, [NotNullWhen(true)] out ResolveResult? result) + { + Log.WriteLine("IsUnambiguousMethodReference: Performing overload resolution for " + method); + + var lookup = CreateLookup(resolver); + OverloadResolution or; + + if (isExtensionMethodReference) + { + result = resolver.ResolveMemberAccess(target, method.Name, typeArguments, NameLookupMode.InvocationTarget) as MethodGroupResolveResult; + if (result == null) + return false; + or = ((MethodGroupResolveResult)result).PerformOverloadResolution(resolver.CurrentTypeResolveContext.Compilation, + method.Parameters.SelectReadOnlyArray(p => new TypeResolveResult(p.Type)), + argumentNames: null, allowExtensionMethods: true); + if (or == null || or.IsAmbiguous) + return false; + } + else + { + // There are no arguments; the parameter types stand in for them, and argument + // names are not possible. + or = CreateOverloadResolution(resolver, + method.Parameters.SelectReadOnlyArray(p => new TypeResolveResult(p.Type)), + argumentNames: null, typeArguments.ToArray()); + if (target == null) + { + result = resolver.ResolveSimpleName(method.Name, typeArguments, isInvocationTarget: false); + if (!(result is MethodGroupResolveResult mgrr)) + return false; + or.AddMethodLists(mgrr.MethodsGroupedByDeclaringType.ToArray()); + } + else + { + result = lookup.Lookup(target, method.Name, typeArguments, isInvocation: false); + if (!(result is MethodGroupResolveResult mgrr)) + return false; + or.AddMethodLists(mgrr.MethodsGroupedByDeclaringType.ToArray()); + } + } + + var foundMethod = or.GetBestCandidateWithSubstitutedTypeArguments(); + if (!IsAppropriateCallTarget(expectedTargetDetails, method, foundMethod)) + return false; + return result is MethodGroupResolveResult; + } + + /// + /// Checks whether calling `target.methodName()` will use `expected` as the method to invoke. + /// + internal static bool CheckSimpleCall(ExpressionBuilder expressionBuilder, ResolveResult target, + IMethod expected, OpCode expectedCallOpCode = OpCode.Call) + { + var details = new ExpectedTargetDetails { CallOpCode = expectedCallOpCode, NeedsBoxingConversion = false }; + CSharpResolver resolver = expressionBuilder.resolver; + if (resolver.ResolveMemberAccess(target, expected.Name, [], NameLookupMode.InvocationTarget) + is not MethodGroupResolveResult mgrr) + return false; + var or = mgrr.PerformOverloadResolution(resolver.Compilation, []); + if (or.BestCandidateErrors != OverloadResolutionErrors.None || or.IsAmbiguous) + return false; + return IsAppropriateCallTarget(details, expected, or.GetBestCandidateWithSubstitutedTypeArguments()!); + } + + internal static bool IsAppropriateCallTarget(ExpectedTargetDetails expectedTargetDetails, IMember expectedTarget, IMember actualTarget) + { + if (expectedTarget.Equals(actualTarget, NormalizeTypeVisitor.TypeErasure)) + return true; + + if (expectedTargetDetails.CallOpCode == OpCode.CallVirt && actualTarget.IsOverride) + { + if (expectedTargetDetails.NeedsBoxingConversion && actualTarget.DeclaringType.IsReferenceType != true) + return false; + foreach (var possibleTarget in InheritanceHelper.GetBaseMembers(actualTarget, false)) + { + if (expectedTarget.Equals(possibleTarget, NormalizeTypeVisitor.TypeErasure)) + return true; + if (!possibleTarget.IsOverride) + break; + } + } + return false; + } + + void ModifyReturnTypeOfLambda(LambdaExpression lambda) + { + var resolveResult = (DecompiledLambdaResolveResult)lambda.GetResolveResult(); + if (lambda.Body is Expression exprBody) + lambda.Body = new TranslatedExpression(exprBody.Detach()).ConvertTo(resolveResult.ReturnType, expressionBuilder); + else + ModifyReturnStatementInsideLambda(resolveResult.ReturnType, lambda); + resolveResult.InferredReturnType = resolveResult.ReturnType; + } + + void CastArguments(IList arguments, IList expectedParameters) + { + for (int i = 0; i < arguments.Count; i++) + { + if (expressionBuilder.settings.AnonymousTypes && expectedParameters[i].Type.ContainsAnonymousType()) + { + if (arguments[i].Expression is LambdaExpression lambda) + { + ModifyReturnTypeOfLambda(lambda); + } + } + else + { + IParameter parameter = expectedParameters[i]; + IType parameterType; + if (parameter.Type.Kind == TypeKind.Dynamic) + { + parameterType = expressionBuilder.compilation.FindType(KnownTypeCode.Object); + } + else + { + parameterType = parameter.Type; + } + + if (parameter.ReferenceKind == ReferenceKind.In && parameterType is ByReferenceType brt && arguments[i].Type is not ByReferenceType) + { + parameterType = brt.ElementType; + } + + arguments[i] = arguments[i].ConvertTo(parameterType, expressionBuilder, allowImplicitConversion: false); + } + } + } + + void EnforceExplicitIn(TranslatedExpression[] arguments, IParameter[] expectedParameters) + { + for (int i = 0; i < arguments.Length; i++) + { + if (expectedParameters[i].ReferenceKind != ReferenceKind.In) + continue; + if (arguments[i].Expression is DirectionExpression) + continue; + + arguments[i] = WrapInAsRefReadOnly(arguments[i]); + expressionBuilder.statementBuilder.EmitAsRefReadOnly = true; + } + } + + TranslatedExpression WrapInAsRefReadOnly(TranslatedExpression arg) + { + return new DirectionExpression( + FieldDirection.In, + new InvocationExpression { + Target = new IdentifierExpression("ILSpyHelper_AsRefReadOnly"), + Arguments = { arg.Expression } + } + ).WithRR(new ByReferenceResolveResult(arg.Type, ReferenceKind.In)) + .WithoutILInstruction(); + } + + void ModifyReturnStatementInsideLambda(IType returnType, AstNode parent) + { + foreach (var child in parent.Children) + { + if (child is LambdaExpression || child is AnonymousMethodExpression) + continue; + if (child is ReturnStatement ret) + { + if (ret.Expression is not null) + ret.Expression = new TranslatedExpression(ret.Expression.Detach()).ConvertTo(returnType, expressionBuilder); + continue; + } + ModifyReturnStatementInsideLambda(returnType, child); + } + } + + bool Apply(ReferenceTransformation step) + { + switch (step) + { + case ReferenceTransformation.RequireTarget: + return true; // recording it in Applied is the whole effect + case ReferenceTransformation.CastTarget: + if (skipTargetCast && RequireTarget != initiallyRequiredTarget) + { + // A protected member of a base type cannot be reached through a cast + // target, so the qualifier the previous step added is worse than useless: + // drop it again and let the remaining escalations do the work. It stays + // spent, so it is not offered a second time. + Applied &= ~ReferenceTransformation.RequireTarget; + return true; + } + Target = Target.ConvertTo(declaringType, expressionBuilder); + return true; + case ReferenceTransformation.RequireTypeArguments: + if (member is not IMethod method) + return false; + TypeArguments = method.TypeArguments.ToArray(); + return true; + case ReferenceTransformation.NoNamedArgsForPrettiness: + if (!Arguments.AddNamesToPrimitiveValues) + return false; + Arguments.AddNamesToPrimitiveValues = false; + return true; + case ReferenceTransformation.NoOptionalArgumentAllowed: + if (Arguments.FirstOptionalArgumentIndex < 0) + return false; + Arguments.FirstOptionalArgumentIndex = -1; + return true; + case ReferenceTransformation.CastArguments: + if (TypeArgumentsWereAppliedUpFront) + { + // The guess did not help, so take it back completely: a later step may + // still reach for it once the casts are in. + TypeArgumentsWereAppliedUpFront = false; + Applied &= ~ReferenceTransformation.RequireTypeArguments; + spent &= ~ReferenceTransformation.RequireTypeArguments; + TypeArguments = Empty.Array; + } + if (castingArgumentsDropsImplicitlyTypedOut) + { + Arguments.UseImplicitlyTypedOut = false; + } + CastArguments(Arguments.Arguments, Arguments.ExpectedParameters); + return true; + case ReferenceTransformation.EnforceExplicitIn: + EnforceExplicitIn(Arguments.Arguments, Arguments.ExpectedParameters); + return true; + default: + return false; + } + } + } +} diff --git a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs index 600ca0b845a..bfa376e8db8 100644 --- a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs @@ -290,13 +290,25 @@ bool HidesVariableOrNestedFunction(ILFunction function) return null; } - bool RequiresQualifier(IMember member, TranslatedExpression target) + /// + /// Whether a reference to has to name its target to reach it. + /// Dropping a "base." qualifier leaves the reference to dispatch virtually, which reaches + /// the same member unless the member can be overridden and the IL did not dispatch + /// virtually - so is what a base target turns on. A + /// reference that does not dispatch at all, such as a field access, never needs it. + /// Overridable, not virtual: an abstract or overriding member is dispatched virtually + /// without carrying the keyword, and a sealed override can no longer be overridden, so + /// dispatching it virtually reaches the same member anyway. + /// + internal bool RequiresQualifier(IMember member, TranslatedExpression target, bool nonVirtualDispatch = false) { if (settings.AlwaysQualifyMemberReferences || HidesVariableWithName(member.Name)) return true; if (member.IsStatic) return !IsCurrentOrContainingType(member.DeclaringTypeDefinition); - return !(target.Expression is ThisReferenceExpression || target.Expression is BaseReferenceExpression); + if (target.Expression is BaseReferenceExpression) + return nonVirtualDispatch && member.IsOverridable; + return target.Expression is not ThisReferenceExpression; } /// @@ -367,46 +379,13 @@ ExpressionWithResolveResult ConvertField(IField field, ILInstruction? targetInst { requireTarget = RequiresQualifier(field, target); } - bool targetCasted = false; - var targetResolveResult = requireTarget ? target.ResolveResult : null; - - bool IsAmbiguousAccess(out MemberResolveResult? result) - { - if (targetResolveResult == null) - { - result = resolver.ResolveSimpleName(field.Name, EmptyList.Instance, isInvocationTarget: false) as MemberResolveResult; - } - else - { - var lookup = new MemberLookup(resolver.CurrentTypeDefinition, resolver.CurrentTypeDefinition.ParentModule); - result = lookup.Lookup(target.ResolveResult, field.Name, EmptyList.Instance, isInvocation: false) as MemberResolveResult; - } - return result == null || result.IsError || !result.Member.Equals(field, NormalizeTypeVisitor.TypeErasure); - } - - MemberResolveResult? mrr; - while (IsAmbiguousAccess(out mrr)) - { - if (!requireTarget) - { - requireTarget = true; - targetResolveResult = target.ResolveResult; - } - else if (!targetCasted) - { - targetCasted = true; - target = target.ConvertTo(field.DeclaringType, this); - targetResolveResult = target.ResolveResult; - } - else - { - // the field reference is still ambiguous, however, mrr might refer to a different member, - // e.g., in the case of auto events, their backing fields have the same name. - // "this.Event" is ambiguous, but should refer to the field, not the event. - mrr = null; - break; - } - } + var disambiguator = Disambiguator.ForField(this, field, target, requireTarget); + // On giving up, the reference stays ambiguous, however the resolved member might be a + // different one, e.g., in the case of auto events, whose backing fields have the same + // name. "this.Event" is ambiguous, but should refer to the field, not the event. + MemberResolveResult? mrr = disambiguator.Resolved ? (MemberResolveResult?)disambiguator.Result : null; + requireTarget = disambiguator.RequireTarget; + target = disambiguator.Target; if (mrr == null || !requireTarget) { @@ -4580,7 +4559,7 @@ protected internal override TranslatedExpression VisitAwait(Await inst, Translat boxedOperand = boxCast.Expression; lookupTarget = boxing.Input; } - if (!callBuilder.CheckSimpleCall(lookupTarget, inst.GetAwaiterMethod, inst.GetAwaiterCallOpCode)) + if (!Disambiguator.CheckSimpleCall(this, lookupTarget, inst.GetAwaiterMethod, inst.GetAwaiterCallOpCode)) { value = value.ConvertTo(expectedType, this); }