diff --git a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs index 86c86cbb7..db7f0e64b 100644 --- a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs @@ -926,6 +926,12 @@ namespace ICSharpCode.Decompiler.Tests await RunForLibrary(cscOptions: cscOptions); } + [Test] + public async Task Issue3965([ValueSource(nameof(roslyn4OrNewerOptions))] CompilerOptions cscOptions) + { + await RunForLibrary(cscOptions: cscOptions); + } + [Test] public async Task AssemblyCustomAttributes([ValueSource(nameof(defaultOptions))] CompilerOptions cscOptions) { diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Issue3965.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Issue3965.cs new file mode 100644 index 000000000..d735cf957 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Issue3965.cs @@ -0,0 +1,29 @@ +using System; + +namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty +{ + internal class Issue3965 + { + private static void M(int x) + { + } + + private static void Use(Action action) + { + } + + public static void DiscardedMethodGroupConversion() + { +#if EXPECTED_OUTPUT + new Action(M); +#else + _ = (Action)M; +#endif + } + + public static void UsedMethodGroupConversion() + { + Use(M); + } + } +} diff --git a/ICSharpCode.Decompiler/IL/Transforms/CachedDelegateInitialization.cs b/ICSharpCode.Decompiler/IL/Transforms/CachedDelegateInitialization.cs index e67f31123..48150bf5e 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/CachedDelegateInitialization.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/CachedDelegateInitialization.cs @@ -37,7 +37,6 @@ namespace ICSharpCode.Decompiler.IL.Transforms { if (CachedDelegateInitializationWithField(inst)) { - block.Instructions.RemoveAt(i); context.IndexOfFirstAlreadyTransformedInstruction = block.Instructions.Count; continue; } @@ -95,13 +94,25 @@ namespace ICSharpCode.Decompiler.IL.Transforms if (!DelegateConstruction.MatchDelegateConstruction(value.UnwrapConv(ConversionKind.Invalid) as NewObj, out _, out _, out _, true)) return false; var nextInstruction = inst.Parent.Children.ElementAtOrDefault(inst.ChildIndex + 1); - if (nextInstruction == null) - return false; - var usages = nextInstruction.Descendants.Where(i => i.MatchLdsFld(field)).ToArray(); + var usages = nextInstruction?.Descendants.Where(i => i.MatchLdsFld(field)).ToArray() ?? []; + if (usages.Length == 0) + { + // A discarded method-group conversion ("_ = (Action)M;") caches the + // delegate without ever reading the cache back. Keep the conversion, + // which still allocates, and drop the caching around it -- but only + // once the rest of the method is known not to read the field either. + if (context.Function.Descendants.Any(i => i != left && i.MatchLdsFld(field))) + return false; + context.Step("CachedDelegateInitializationWithField (unused)", inst); + inst.ReplaceWith(value); + context.EndStep(value); + return true; + } if (usages.Length != 1) return false; context.Step("CachedDelegateInitializationWithField", inst); usages[0].ReplaceWith(value); + ((Block)inst.Parent).Instructions.RemoveAt(inst.ChildIndex); context.EndStep(value); return true; }