From f007528af9fc4c610342df7d4d4f7afab5acbef1 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Tue, 1 Sep 2026 17:53:31 +0200 Subject: [PATCH] Fix #3008: local function forwarding a display class loses its scope Roslyn passes a display class into a local function by ref, and a local function that only forwards that parameter to a sibling has no closure variable of its own. The closure analysis therefore found nothing to anchor it and fell back to the root method body, which put it out of reach of the callees it forwards to; CallBuilder then hit the assert guarding a local function reference it cannot resolve and emitted the raw metadata name of the target instead. The constructor path also mixed use-site containers into a scope the closure analysis had already determined; when the use-sites live in separate function bodies there is no common container, and resetting to the constructor body threw that scope away. Assisted-by: Claude:claude-opus-5:Claude Code --- .../TestCases/Pretty/LocalFunctions.cs | 24 +++++++++ .../IL/Transforms/LocalFunctionDecompiler.cs | 51 +++++++++++++------ 2 files changed, 60 insertions(+), 15 deletions(-) diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/LocalFunctions.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/LocalFunctions.cs index d16598e09..258958b6e 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/LocalFunctions.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/LocalFunctions.cs @@ -902,5 +902,29 @@ namespace LocalFunctions #endif return outer; } + + public void Issue3008_DisplayClassForwardedThroughLocalFunction(int start) + { + Run(); + void Run() + { + int captured = start; + A(); + Console.WriteLine(captured); + void A() + { + captured++; + if (captured < 10) + { + B(); + } + } + void B() + { + captured += 2; + A(); + } + } + } } } diff --git a/ICSharpCode.Decompiler/IL/Transforms/LocalFunctionDecompiler.cs b/ICSharpCode.Decompiler/IL/Transforms/LocalFunctionDecompiler.cs index 1a54a88ab..3ef8dda11 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/LocalFunctionDecompiler.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/LocalFunctionDecompiler.cs @@ -141,22 +141,27 @@ namespace ICSharpCode.Decompiler.IL.Transforms foreach (var useSite in info.UseSites) { DetermineCaptureAndDeclarationScope(info, useSite); + } - if (context.Function.Method.IsConstructor) + if (context.Function.Method.IsConstructor) + { + // Local functions reached from a field initializer usually capture nothing, so + // the closure analysis leaves no scope behind; the innermost block containing + // all use-sites is a better place for them than the whole constructor body. + // Use-sites spread over separate function bodies have no common block + // container at all; there the scope from the closure analysis stands. + BlockContainer useSiteScope = null; + foreach (var useSite in info.UseSites) { - if (localFunction.DeclarationScope == null) - { - localFunction.DeclarationScope = BlockContainer.FindClosestContainer(useSite); - } - else - { - localFunction.DeclarationScope = FindCommonAncestorInstruction(useSite, localFunction.DeclarationScope); - if (localFunction.DeclarationScope == null) - { - localFunction.DeclarationScope = (BlockContainer)context.Function.Body; - } - } + useSiteScope = useSiteScope == null + ? BlockContainer.FindClosestContainer(useSite) + : FindCommonAncestorInstruction(useSite, useSiteScope); + if (useSiteScope == null) + break; } + localFunction.DeclarationScope = useSiteScope + ?? localFunction.DeclarationScope + ?? (BlockContainer)context.Function.Body; } if (localFunction.DeclarationScope == null) @@ -341,9 +346,18 @@ namespace ICSharpCode.Decompiler.IL.Transforms return null; if (!(TransformDisplayClassUsage.IsPotentialClosure(context, field.Type.GetDefinition()) || context.Function.Method.DeclaringType.Equals(field.Type))) return null; - foreach (var v in context.Function.Descendants.OfType().SelectMany(f => f.Variables)) + return FindClosureVariableOfType(field.Type); + } + + /// + /// Finds the variable holding the display class instance of the given type, anywhere in the + /// function tree currently being decompiled. + /// + private ILVariable FindClosureVariableOfType(IType type) + { + foreach (var v in context.Function.Descendants.OfType().Prepend(context.Function).SelectMany(f => f.Variables)) { - if (!(TransformDisplayClassUsage.IsClosure(context, v, out var varType, out _) && varType.Equals(field.Type))) + if (!(TransformDisplayClassUsage.IsClosure(context, v, out var varType, out _) && varType.Equals(type))) continue; return v; } @@ -713,6 +727,13 @@ namespace ICSharpCode.Decompiler.IL.Transforms } if (closureVar.Kind == VariableKind.NamedArgument) return false; + if (parameterIndex >= 0 && closureVar.Kind == VariableKind.Parameter) + { + // The use-site sits inside another local function that received the display class as + // a parameter and forwards it. A parameter has no initializer, so the scope the + // display class was created in has to be recovered from the variable holding it. + closureVar = FindClosureVariableOfType(closureVar.Type.UnwrapByRef()) ?? closureVar; + } var initializer = GetClosureInitializer(closureVar); if (initializer == null) return false;