Browse Source

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
pull/3980/head
Siegfried Pammer 1 month ago
parent
commit
13c6cc6741
  1. 17
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/ConstructorInitializers.cs
  2. 14
      ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs

17
ICSharpCode.Decompiler.Tests/TestCases/Pretty/ConstructorInitializers.cs

@ -147,6 +147,23 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty @@ -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

14
ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs

@ -2788,6 +2788,20 @@ namespace ICSharpCode.Decompiler.CSharp @@ -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;
}
}

Loading…
Cancel
Save