From 13c6cc6741aa1b0dd38dcec42c3d5f3e5e4bd3fd Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Wed, 12 Aug 2026 09:19:21 +0200 Subject: [PATCH] Give an explicit `this` the same resolve result as `base` The guard added here reads the target of a member access to decide whether an assignment may move into a field initializer, and it recognises the current instance as a ThisResolveResult. Only one of the two spellings produces that. An unqualified `A` is resolved through CSharpResolver.LookInCurrentType, which synthesizes the target as a this-reference; an explicit `this.A` is built by ExpressionBuilder, whose TranslateTarget hands back whatever ConvertVariable produced - and `this` is a parameter like any other there, so the target is an ILVariableResolveResult. The guard saw the first and missed the second. Which spelling appears is decided by RequiresQualifier, for reasons unrelated to the question being asked: a constructor parameter that shadows the field forces the qualifier, and AlwaysQualifyMemberReferences forces it everywhere. So the transform hoisted `b = this.value + 1` into a field initializer, where naming the instance is CS0027 and the output does not compile. TranslateTarget already builds a ThisResolveResult for `base`, one branch above. Doing the same for `this` leaves the guard untouched and makes it see both spellings, and spares every future consumer the same trap. The type is carried over from the previous resolve result, so nothing downstream observes a different one - the this/base keyword links read exactly this node. Fixes #3984. Assisted-by: Claude:claude-opus-5:Claude Code --- .../TestCases/Pretty/ConstructorInitializers.cs | 17 +++++++++++++++++ .../CSharp/ExpressionBuilder.cs | 14 ++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ConstructorInitializers.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ConstructorInitializers.cs index c4b3f3f6f..0be38a28c 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ConstructorInitializers.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ConstructorInitializers.cs @@ -147,6 +147,23 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty B = A + 1; } } + + private struct StructInitializerDependsOnQualifiedField + { + public int value; + + public int b; + + // The parameter shadows the field, so reading the field keeps its qualifier. A + // field initializer cannot name the instance, so this has to stay a constructor + // rather than becoming a primary one - the same rule as the fixture above, which + // only differs in how the read happens to be spelled. + public StructInitializerDependsOnQualifiedField(int value) + { + this.value = value; + b = this.value + 1; + } + } #endif public class ClassWithConstant diff --git a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs index 3df219962..6d1e9ef7a 100644 --- a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs @@ -2788,6 +2788,20 @@ namespace ICSharpCode.Decompiler.CSharp .WithoutILInstruction(); } translatedTarget = EnsureTargetNotNullable(translatedTarget, target); + if (translatedTarget.Expression is ThisReferenceExpression) + { + // Give an explicit `this` the same resolve result the base-reference branch + // above gives `base`, and that the resolver gives the unqualified spelling of + // the same access. ConvertVariable annotates it as an ordinary local, so + // without this a consumer asking "does this expression reach instance state" + // gets a different answer depending on whether the qualifier happened to be + // printed - and the qualifier is printed for reasons (a parameter of the same + // name, AlwaysQualifyMemberReferences) that have nothing to do with the + // question being asked. + translatedTarget = new ThisReferenceExpression() + .WithILInstruction(target) + .WithRR(new ThisResolveResult(translatedTarget.Type, nonVirtualInvocation)); + } return translatedTarget; } }