From 7ccb8f49774aefb936c1b741e34a21f4cf92a920 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Sun, 5 Jul 2026 20:57:56 +0200 Subject: [PATCH 1/2] Fix #3860: Avoid 'out var' if the variable recurs in the argument list Passing the same local as multiple out arguments of one call made DeclareVariables turn the first use into an implicitly-typed declaration, producing 'f(out var x, out x)'. Referencing an implicitly-typed out variable in another argument of the declaring call is rejected by the compiler (CS8196), because its type is only inferred once overload resolution of that call has completed. The explicitly-typed form 'f(out int x, out x)' is valid, so fall back to the explicit type in that case. Assisted-by: Claude:claude-fable-5:Claude Code --- .../TestCases/Pretty/OutVariables.cs | 19 +++++++++ .../CSharp/Transforms/DeclareVariables.cs | 42 ++++++++++++++++++- 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/OutVariables.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/OutVariables.cs index 5041dbcc91..45cb081ab0 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/OutVariables.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/OutVariables.cs @@ -92,5 +92,24 @@ public static void CapturedBoolResult(Dictionary d, int key) Console.WriteLine(value2); Console.WriteLine(value2); } + + private static void GetTwo(out int a, out int b) + { + a = 1; + b = 2; + } + + public static void SameVariableUsedForTwoOutParameters() + { + // The declaration must use the explicit type: referencing an implicitly-typed + // out variable in another argument of the declaring call is an error (CS8196). + GetTwo(out int a, out a); + } + + public static int SameVariableUsedForTwoOutParametersAndRead() + { + GetTwo(out int a, out a); + return a; + } } } diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/DeclareVariables.cs b/ICSharpCode.Decompiler/CSharp/Transforms/DeclareVariables.cs index 8fbc4b9a32..fe3c2e70eb 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/DeclareVariables.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/DeclareVariables.cs @@ -651,7 +651,8 @@ void InsertVariableDeclarations(TransformContext context) type = new SimpleType("var"); isOutVar = true; } - else if (dirExpr.Annotation() != null) + else if (dirExpr.Annotation() != null + && !IsReferencedWithinDeclaringCall(dirExpr, v)) { type = new SimpleType("var"); isOutVar = true; @@ -788,6 +789,45 @@ void InsertVariableDeclarations(TransformContext context) } } + /// + /// Gets whether the variable declared by is referenced again within + /// another argument of the call containing the declaration. In that case the declaration must use + /// the explicit type: referencing an implicitly-typed out variable is not permitted until overload + /// resolution of the declaring call has inferred its type (CS8196). + /// + bool IsReferencedWithinDeclaringCall(DirectionExpression dirExpr, VariableToDeclare v) + { + AstNode? call = dirExpr.Parent; + if (call == null) + return false; + for (AstNode? argument = call.FirstChild; argument != null; argument = argument.NextSibling) + { + if (argument == dirExpr) + continue; + foreach (AstNode node in argument.DescendantsAndSelf) + { + if (node is IdentifierExpression identifier && ResolveVariableToDeclare(identifier.GetILVariable()) == v) + return true; + } + } + return false; + } + + /// + /// Maps an ILVariable to the variable declaration it will end up in, following merges + /// performed by ResolveCollisions. + /// + VariableToDeclare? ResolveVariableToDeclare(ILVariable? variable) + { + if (variable == null || !variableDict.TryGetValue(variable, out VariableToDeclare? v)) + return null; + while (v.ReplacementDueToCollision is { } replacement) + { + v = replacement; + } + return v; + } + private bool CanBeDeclaredAsOutVariable(VariableToDeclare v, [NotNullWhen(true)] out DirectionExpression? dirExpr) { dirExpr = v.FirstUse.Parent as DirectionExpression; From 62c8d3b00a814386d697f8bdb410f84bbcbf666c Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Mon, 6 Jul 2026 22:24:16 +0200 Subject: [PATCH 2/2] Add test for out-var reference nested within another argument CS8196 also fires when the second reference sits inside a nested call in another argument of the declaring call, e.g. OutAndValue(out var a, UseAndReturn(out a)). The existing check already covers this because it walks all descendants of the sibling arguments; this fixture pins that behavior. Assisted-by: Claude:claude-fable-5:Claude Code --- .../TestCases/Pretty/OutVariables.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/OutVariables.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/OutVariables.cs index 45cb081ab0..a7cfc0d428 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/OutVariables.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/OutVariables.cs @@ -111,5 +111,24 @@ public static int SameVariableUsedForTwoOutParametersAndRead() GetTwo(out int a, out a); return a; } + + private static void OutAndValue(out int a, int b) + { + a = b; + } + + private static int UseAndReturn(out int a) + { + a = 1; + return a; + } + + public static void SameVariableUsedInNestedCallArgument() + { + // CS8196 also applies when the second reference is nested within + // another argument of the declaring call. + OutAndValue(out int a, UseAndReturn(out a)); + Console.WriteLine(a); + } } }