Browse Source

Merge pull request #4008 from icsharpcode/feature/this-resolve-result-ilvariable

Make ThisResolveResult an ILVariableResolveResult of the this parameter
pull/4010/head
Christoph Wille 1 month ago committed by GitHub
parent
commit
9cdbbb45aa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 141
      ICSharpCode.Decompiler.Tests/Semantics/ThisResolveResultTests.cs
  2. 28
      ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs
  3. 53
      ICSharpCode.Decompiler/CSharp/Resolver/CSharpResolver.cs
  4. 14
      ICSharpCode.Decompiler/IL/Instructions/PatternMatching.cs
  5. 9
      ICSharpCode.Decompiler/Semantics/ThisResolveResult.cs

141
ICSharpCode.Decompiler.Tests/Semantics/ThisResolveResultTests.cs

@ -0,0 +1,141 @@ @@ -0,0 +1,141 @@
// Copyright (c) 2026 Christoph Wille
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Linq;
using ICSharpCode.Decompiler.CSharp;
using ICSharpCode.Decompiler.CSharp.Syntax;
using ICSharpCode.Decompiler.IL;
using ICSharpCode.Decompiler.Metadata;
using ICSharpCode.Decompiler.Semantics;
using ICSharpCode.Decompiler.Tests.TypeSystem;
using ICSharpCode.Decompiler.TypeSystem;
using NUnit.Framework;
namespace ICSharpCode.Decompiler.Tests.Semantics
{
// Sample hierarchy decompiled by ThisResolveResultTests: every way of naming the current
// instance (unqualified member, explicit 'this', 'base', a struct's by-ref 'this') has to
// end up annotated with the same 'this' parameter of the method being decompiled.
internal class ThisReferenceBase
{
public virtual int Get() => 0;
}
internal class ThisReferenceSample : ThisReferenceBase
{
public int value;
public ThisReferenceSample(int value)
{
// The parameter shadows the field, so this access keeps its qualifier.
this.value = value;
}
public int Unqualified() => value;
public override int Get() => base.Get();
}
internal struct ThisReferenceStructSample
{
public int value;
public int Unqualified() => value;
}
[TestFixture]
public class ThisResolveResultTests
{
// All samples live in this test assembly, so a single decompiler (and the PE file
// shared with the other fixtures) serves every test here. CSharpDecompiler is not
// safe for concurrent decompilations, so this fixture must stay non-parallelizable;
// marking it [Parallelizable] requires a decompiler per test.
static readonly Lazy<CSharpDecompiler> decompiler = new Lazy<CSharpDecompiler>(
delegate {
var module = TypeSystemLoaderTests.TestAssembly;
var resolver = new UniversalAssemblyResolver(module.FileName, false, module.Metadata.DetectTargetFrameworkId());
return new CSharpDecompiler(module, resolver, new DecompilerSettings());
});
static SyntaxTree Decompile(System.Type type)
{
return decompiler.Value.DecompileType(new FullTypeName(type.FullName));
}
static ILVariable AssertIsThisParameter(ResolveResult rr, string what)
{
Assert.That(rr, Is.InstanceOf<ThisResolveResult>(), what);
var variable = (rr as ILVariableResolveResult)?.Variable;
Assert.That(variable, Is.Not.Null, what);
Assert.That(variable.IsThis(), Is.True, what);
return variable;
}
static ResolveResult ResolveResultOf(SyntaxTree tree, string methodName, System.Func<MethodDeclaration, Expression> select)
{
var method = tree.Descendants.OfType<MethodDeclaration>().Single(m => m.Name == methodName);
return select(method).GetResolveResult();
}
[Test]
public void ExplicitThisIsTheThisParameter()
{
var tree = Decompile(typeof(ThisReferenceSample));
var ctor = tree.Descendants.OfType<ConstructorDeclaration>().Single();
var thisRef = ctor.Descendants.OfType<ThisReferenceExpression>().Single();
var variable = AssertIsThisParameter(thisRef.GetResolveResult(), "this");
Assert.That(variable.Type.FullName, Is.EqualTo(typeof(ThisReferenceSample).FullName));
}
[Test]
public void UnqualifiedFieldAccessTargetsTheThisParameter()
{
var tree = Decompile(typeof(ThisReferenceSample));
var rr = ResolveResultOf(tree, "Unqualified", m => m.Descendants.OfType<IdentifierExpression>().Single(id => id.Identifier == "value"));
var mrr = rr as MemberResolveResult;
Assert.That(mrr, Is.Not.Null, "unqualified field access");
AssertIsThisParameter(mrr.TargetResult, "target of unqualified field access");
}
[Test]
public void BaseReferenceIsTheThisParameterWithTheBaseType()
{
var tree = Decompile(typeof(ThisReferenceSample));
var rr = ResolveResultOf(tree, "Get", m => m.Descendants.OfType<BaseReferenceExpression>().Single());
var variable = AssertIsThisParameter(rr, "base");
Assert.That(rr.Type.FullName, Is.EqualTo(typeof(ThisReferenceBase).FullName));
Assert.That(variable.Type.FullName, Is.EqualTo(typeof(ThisReferenceSample).FullName));
Assert.That(((ThisResolveResult)rr).CausesNonVirtualInvocation, Is.True);
}
[Test]
public void StructUnqualifiedFieldAccessTargetsTheByRefThisParameter()
{
var tree = Decompile(typeof(ThisReferenceStructSample));
var rr = ResolveResultOf(tree, "Unqualified", m => m.Descendants.OfType<IdentifierExpression>().Single(id => id.Identifier == "value"));
var mrr = rr as MemberResolveResult;
Assert.That(mrr, Is.Not.Null, "unqualified field access");
var variable = AssertIsThisParameter(mrr.TargetResult, "target of unqualified field access");
Assert.That(variable.Type, Is.InstanceOf<ByReferenceType>());
Assert.That(mrr.TargetResult.Type.Kind, Is.Not.EqualTo(TypeKind.ByReference), "the reference is spelled as the struct itself");
}
}
}

28
ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs

@ -423,8 +423,11 @@ namespace ICSharpCode.Decompiler.CSharp @@ -423,8 +423,11 @@ namespace ICSharpCode.Decompiler.CSharp
}
}
if (mrr == null)
if (mrr == null || !requireTarget)
{
// The resolver looked the unqualified name up against the current type, so its
// result does not carry the translated target; annotate the same this/base or
// type target the qualified spelling gets.
mrr = new MemberResolveResult(target.ResolveResult, field);
}
@ -2831,13 +2834,13 @@ namespace ICSharpCode.Decompiler.CSharp @@ -2831,13 +2834,13 @@ namespace ICSharpCode.Decompiler.CSharp
// Additionally check target for null, in order to avoid a crash.
if (!memberStatic && target != null)
{
if (ShouldUseBaseReference())
if (ShouldUseBaseReference(out var baseThisVariable))
{
var baseReferenceType = resolver.CurrentTypeDefinition.DirectBaseTypes
.FirstOrDefault(t => t.Kind != TypeKind.Interface);
return new BaseReferenceExpression()
.WithILInstruction(target)
.WithRR(new ThisResolveResult(baseReferenceType ?? memberDeclaringType, nonVirtualInvocation));
.WithRR(new ThisResolveResult(baseThisVariable, baseReferenceType ?? memberDeclaringType, nonVirtualInvocation));
}
else
{
@ -2881,11 +2884,11 @@ namespace ICSharpCode.Decompiler.CSharp @@ -2881,11 +2884,11 @@ namespace ICSharpCode.Decompiler.CSharp
.WithoutILInstruction();
}
translatedTarget = EnsureTargetNotNullable(translatedTarget, target);
if (translatedTarget.Expression is ThisReferenceExpression)
if (translatedTarget.Expression is ThisReferenceExpression
&& translatedTarget.ResolveResult is ILVariableResolveResult { Variable: var thisVariable })
{
// 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
// above gives `base`. 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
@ -2893,7 +2896,7 @@ namespace ICSharpCode.Decompiler.CSharp @@ -2893,7 +2896,7 @@ namespace ICSharpCode.Decompiler.CSharp
// question being asked.
translatedTarget = new ThisReferenceExpression()
.WithILInstruction(target)
.WithRR(new ThisResolveResult(translatedTarget.Type, nonVirtualInvocation));
.WithRR(new ThisResolveResult(thisVariable, translatedTarget.Type, nonVirtualInvocation));
}
return translatedTarget;
}
@ -2905,21 +2908,22 @@ namespace ICSharpCode.Decompiler.CSharp @@ -2905,21 +2908,22 @@ namespace ICSharpCode.Decompiler.CSharp
.WithRR(new TypeResolveResult(constrainedTo ?? memberDeclaringType));
}
bool ShouldUseBaseReference()
bool ShouldUseBaseReference([NotNullWhen(true)] out ILVariable? thisVariable)
{
thisVariable = null;
if (!nonVirtualInvocation)
return false;
if (!MatchLdThis(target))
if (!MatchLdThis(target, out thisVariable))
return false;
if ((constrainedTo ?? memberDeclaringType).GetDefinition() == resolver.CurrentTypeDefinition)
return false;
return true;
}
bool MatchLdThis(ILInstruction inst)
bool MatchLdThis(ILInstruction inst, [NotNullWhen(true)] out ILVariable? thisVariable)
{
// ldloc this
if (inst.MatchLdThis())
if (inst.MatchLdThis(out thisVariable))
return true;
if (resolver.CurrentTypeDefinition.Kind == TypeKind.Struct)
{
@ -2930,7 +2934,7 @@ namespace ICSharpCode.Decompiler.CSharp @@ -2930,7 +2934,7 @@ namespace ICSharpCode.Decompiler.CSharp
return false;
if (!type.Equals(type2) || !type.Equals(resolver.CurrentTypeDefinition))
return false;
return arg2.MatchLdThis();
return arg2.MatchLdThis(out thisVariable);
}
return false;
}

53
ICSharpCode.Decompiler/CSharp/Resolver/CSharpResolver.cs

@ -1640,8 +1640,15 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver @@ -1640,8 +1640,15 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver
ResolveResult r;
if (lookupMode == NameLookupMode.Expression || lookupMode == NameLookupMode.InvocationTarget)
{
var targetResolveResult = (t == this.CurrentTypeDefinition ? ResolveThisReference() : new TypeResolveResult(t));
r = lookup.Lookup(targetResolveResult, identifier, typeArguments, lookupMode == NameLookupMode.InvocationTarget);
// A ThisResolveResult names the 'this' parameter of an ILFunction, which the
// resolver does not know; the lookup on the current type does not need one,
// as it accepts protected members of the current type on its own. The type
// is self-parameterized so that its members come out specialized, matching
// the members that references from inside the type resolve to.
IType targetType = t == this.CurrentTypeDefinition && t.TypeParameterCount != 0
? new ParameterizedType(t, t.TypeParameters)
: t;
r = lookup.Lookup(new TypeResolveResult(targetType), identifier, typeArguments, lookupMode == NameLookupMode.InvocationTarget);
}
else
{
@ -2621,48 +2628,6 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver @@ -2621,48 +2628,6 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver
}
#endregion
#region Resolve This/Base Reference
/// <summary>
/// Resolves 'this'.
/// </summary>
public ResolveResult ResolveThisReference()
{
ITypeDefinition t = CurrentTypeDefinition;
if (t != null)
{
if (t.TypeParameterCount != 0)
{
// Self-parameterize the type
return new ThisResolveResult(new ParameterizedType(t, t.TypeParameters));
}
else
{
return new ThisResolveResult(t);
}
}
return ErrorResult;
}
/// <summary>
/// Resolves 'base'.
/// </summary>
public ResolveResult ResolveBaseReference()
{
ITypeDefinition t = CurrentTypeDefinition;
if (t != null)
{
foreach (IType baseType in t.DirectBaseTypes)
{
if (baseType.Kind != TypeKind.Unknown && baseType.Kind != TypeKind.Interface)
{
return new ThisResolveResult(baseType, causesNonVirtualInvocation: true);
}
}
}
return ErrorResult;
}
#endregion
#region ResolveConditional
/// <summary>
/// Converts the input to <c>bool</c> using the rules for boolean expressions.

14
ICSharpCode.Decompiler/IL/Instructions/PatternMatching.cs

@ -116,8 +116,18 @@ namespace ICSharpCode.Decompiler.IL @@ -116,8 +116,18 @@ namespace ICSharpCode.Decompiler.IL
public bool MatchLdThis()
{
var inst = this as LdLoc;
return inst != null && inst.Variable.Kind == VariableKind.Parameter && inst.Variable.Index < 0;
return MatchLdThis(out _);
}
public bool MatchLdThis([NotNullWhen(true)] out ILVariable? variable)
{
if (this is LdLoc inst && inst.Variable.IsThis())
{
variable = inst.Variable;
return true;
}
variable = null;
return false;
}
public bool MatchStLoc([NotNullWhen(true)] out ILVariable? variable)

9
ICSharpCode.Decompiler/Semantics/ThisResolveResult.cs

@ -16,6 +16,8 @@ @@ -16,6 +16,8 @@
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using ICSharpCode.Decompiler.CSharp;
using ICSharpCode.Decompiler.IL;
using ICSharpCode.Decompiler.TypeSystem;
namespace ICSharpCode.Decompiler.Semantics
@ -23,12 +25,15 @@ namespace ICSharpCode.Decompiler.Semantics @@ -23,12 +25,15 @@ namespace ICSharpCode.Decompiler.Semantics
/// <summary>
/// Represents the 'this' reference.
/// Also used for the 'base' reference.
/// Both read the 'this' parameter of the current function, so this is also the
/// <see cref="ILVariableResolveResult"/> of that variable. The type is the one the
/// reference is spelled with: 'base' carries the base type, not the variable's type.
/// </summary>
public class ThisResolveResult : ResolveResult
public class ThisResolveResult : ILVariableResolveResult
{
bool causesNonVirtualInvocation;
public ThisResolveResult(IType type, bool causesNonVirtualInvocation = false) : base(type)
public ThisResolveResult(ILVariable thisVariable, IType type, bool causesNonVirtualInvocation = false) : base(thisVariable, type)
{
this.causesNonVirtualInvocation = causesNonVirtualInvocation;
}

Loading…
Cancel
Save